diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5cd143..4836ead 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,8 +82,11 @@ jobs: yarn build:android build-ios: + # Vendored TalsecRuntime fallback (FREERASP_DISABLE_SPM=1). runs-on: macos-latest needs: build-library + env: + FREERASP_DISABLE_SPM: "1" steps: - name: Checkout uses: actions/checkout@v6 @@ -99,3 +102,25 @@ jobs: working-directory: ./example run: | yarn build:ios + + build-ios-spm: + # SPM path (default, dynamic frameworks). + runs-on: macos-latest + needs: build-library + env: + USE_FRAMEWORKS: dynamic + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup + uses: ./.github/actions/setup + + - name: Install pods (SPM) + working-directory: ./example/ios + run: pod install + + - name: Build example for iOS (SPM) + working-directory: ./example + run: | + yarn build:ios diff --git a/example/ios/Podfile b/example/ios/Podfile index e038f35..8bce2a4 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -8,11 +8,12 @@ require Pod::Executable.execute_command('node', ['-p', platform :ios, min_ios_version_supported prepare_react_native_project! -linkage = ENV['USE_FRAMEWORKS'] -if linkage != nil - Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green - use_frameworks! :linkage => linkage.to_sym -end +# freerasp-react-native delivers TalsecRuntime via Swift Package Manager, which +# requires dynamically linked frameworks. Default to dynamic; USE_FRAMEWORKS may +# override the linkage. +linkage = (ENV['USE_FRAMEWORKS'] || 'dynamic').to_sym +Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green +use_frameworks! :linkage => linkage target 'FreeraspRNExample' do config = use_native_modules! @@ -31,5 +32,11 @@ target 'FreeraspRNExample' do :mac_catalyst_enabled => false, # :ccache_enabled => true ) + + # freeRASP: embed TalsecRuntime into the app target on the SPM path (the helper is a + # no-op / cleanup on the vendored fallback, so it is safe to call unconditionally). + # Real apps resolve the helper from node_modules; this example requires it from the repo root. + require_relative '../../freerasp_spm.rb' + freerasp_embed_talsec_spm!(installer) end end diff --git a/freerasp-react-native.podspec b/freerasp-react-native.podspec index 9d17785..2ffc722 100644 --- a/freerasp-react-native.podspec +++ b/freerasp-react-native.podspec @@ -3,6 +3,12 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' +# TalsecRuntime: SPM by default (RN >= 0.75), vendored xcframework fallback. +# Opt out of SPM with FREERASP_DISABLE_SPM=1. Pin the EXACT TalsecRuntime version this +# library release is built against (each RN version maps to a specific TalsecRuntime). +talsec_spm_url = 'https://github.com/talsec/Free-RASP-ReactNative-SPM' +talsec_spm_version = '6.14.4' + Pod::Spec.new do |s| s.name = "freerasp-react-native" s.version = package["version"] @@ -14,14 +20,35 @@ Pod::Spec.new do |s| s.platforms = { :ios => "11.0" } s.source = { :git => "https://github.com/talsec/freerasp-react-native.git", :tag => "#{s.version}" } - s.source_files = 'ios/models/*.{h,m,mm,swift}', - 'ios/utils/*.{h,m,mm,swift}', - 'ios/dispatchers/*.{h,m,mm,swift}', - 'ios/*.{h,m,mm,swift}', - 'ios/TalsecRuntime.xcframework' - s.xcconfig = { 'OTHER_LDFLAGS' => '-framework TalsecRuntime' } - s.ios.vendored_frameworks = "ios/TalsecRuntime.xcframework" - + # SPM is the default whenever the spm_dependency helper is available (RN >= 0.75), + # unless the consumer opts out with FREERASP_DISABLE_SPM=1. + use_spm = respond_to?(:spm_dependency, true) && ENV['FREERASP_DISABLE_SPM'] != '1' + + source_globs = [ + 'ios/models/*.{h,m,mm,swift}', + 'ios/utils/*.{h,m,mm,swift}', + 'ios/dispatchers/*.{h,m,mm,swift}', + 'ios/*.{h,m,mm,swift}', + ] + + if use_spm + # SPM injects the reference into the Pods project; the framework is embedded into the + # app target by freerasp_embed_talsec_spm! (freerasp_spm.rb), called from the Podfile. + s.ios.deployment_target = '13.0' + spm_dependency(s, + url: talsec_spm_url, + requirement: { kind: 'exactVersion', version: talsec_spm_version }, + products: ['TalsecRuntime'] + ) + else + # Vendored xcframework fallback (RN < 0.75 or FREERASP_DISABLE_SPM=1). + source_globs << 'ios/TalsecRuntime.xcframework' + s.xcconfig = { 'OTHER_LDFLAGS' => '-framework TalsecRuntime' } + s.ios.vendored_frameworks = 'ios/TalsecRuntime.xcframework' + end + + s.source_files = source_globs + # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. if respond_to?(:install_modules_dependencies, true) diff --git a/freerasp_spm.rb b/freerasp_spm.rb new file mode 100644 index 0000000..7ec91db --- /dev/null +++ b/freerasp_spm.rb @@ -0,0 +1,53 @@ +# freeRASP — embeds the SPM-delivered TalsecRuntime into the app target(s). spm_dependency +# only attaches it to the pod target, so dyld fails at launch otherwise. Idempotent and safe +# to call unconditionally: removes any stale reference, re-adds only when SPM is active. +# Keep url/requirement in sync with freerasp-react-native.podspec. + +def freerasp_embed_talsec_spm!(installer, + url: 'https://github.com/talsec/Free-RASP-ReactNative-SPM', + requirement: { kind: 'exactVersion', version: '6.14.4' }, + product: 'TalsecRuntime') + + pkg_class = Xcodeproj::Project::Object::XCRemoteSwiftPackageReference + ref_class = Xcodeproj::Project::Object::XCSwiftPackageProductDependency + + # Mirror the podspec: SPM active unless unavailable (RN < 0.75) or opted out. + spm_active = respond_to?(:spm_dependency, true) && ENV['FREERASP_DISABLE_SPM'] != '1' + + installer.aggregate_targets.each do |aggregate_target| + project = aggregate_target.user_project + next if project.nil? + + app_targets = aggregate_target.user_targets.select do |t| + t.respond_to?(:product_type) && t.product_type == 'com.apple.product-type.application' + end + + # Remove any previously-added reference first (avoids a double embed on delivery switch). + app_targets.each do |target| + target.package_product_dependencies.delete_if do |r| + r.class == ref_class && r.product_name == product && + r.package.respond_to?(:repositoryURL) && r.package.repositoryURL == url + end + end + project.root_object.package_references.delete_if do |p| + p.class == pkg_class && p.repositoryURL == url + end + + if spm_active + pkg = project.new(pkg_class) + pkg.repositoryURL = url + pkg.requirement = requirement + project.root_object.package_references << pkg + + app_targets.each do |target| + Pod::UI.puts "[freeRASP][SPM] Embedding #{product} into app target #{target.name}" + dep = project.new(ref_class) + dep.package = pkg + dep.product_name = product + target.package_product_dependencies << dep + end + end + + project.save + end +end diff --git a/plugin/build/index.d.ts b/plugin/build/index.d.ts index a03766e..078b0fa 100644 --- a/plugin/build/index.d.ts +++ b/plugin/build/index.d.ts @@ -1,3 +1,4 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; import { type PluginConfigType } from './pluginConfig'; declare const _default: ConfigPlugin; export default _default; diff --git a/plugin/build/index.js b/plugin/build/index.js index bc07ecc..1d13d91 100644 --- a/plugin/build/index.js +++ b/plugin/build/index.js @@ -1,6 +1,8 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const config_plugins_1 = require("@expo/config-plugins"); +const fs_1 = require("fs"); +const path_1 = require("path"); const { createBuildGradlePropsConfigPlugin } = config_plugins_1.AndroidConfig.BuildProperties; const urlFreerasp = 'https://europe-west3-maven.pkg.dev/talsec-artifact-repository/freerasp'; const urlJitpack = 'https://www.jitpack.io'; @@ -71,10 +73,66 @@ const withAndroidR8Version = (expoConfig, props) => { return config; }); }; +// iOS — SPM delivery of TalsecRuntime (default on RN >= 0.75). Sets dynamic frameworks +// and injects a guarded TalsecRuntime embed into the Podfile post_install (skipped on the +// vendored fallback / FREERASP_DISABLE_SPM=1). Note: the Expo prebuild flow is less +// battle-tested than bare React Native. +const FREERASP_SPM_EMBED_TAG = '# @generated freerasp-react-native (SPM embed)'; +/** + * Force dynamically linked frameworks — required by `spm_dependency`. + */ +const withFreeraspIosDynamicFrameworks = (config) => { + return (0, config_plugins_1.withPodfileProperties)(config, (cfg) => { + cfg.modResults['ios.useFrameworks'] = 'dynamic'; + return cfg; + }); +}; +/** + * Inject the TalsecRuntime embed step into the generated Podfile `post_install`, + * so the SPM binary framework ends up in the app bundle (otherwise dyld fails at launch). + */ +const withFreeraspIosSpmEmbed = (config) => { + return (0, config_plugins_1.withDangerousMod)(config, [ + 'ios', + (cfg) => { + const podfilePath = (0, path_1.join)(cfg.modRequest.platformProjectRoot, 'Podfile'); + let contents = (0, fs_1.readFileSync)(podfilePath, 'utf-8'); + if (!contents.includes(FREERASP_SPM_EMBED_TAG)) { + const anchor = 'post_install do |installer|'; + const anchorIndex = contents.indexOf(anchor); + if (anchorIndex === -1) { + config_plugins_1.WarningAggregator.addWarningIOS('freerasp-react-native', 'Could not find a `post_install` block in the Podfile to inject the ' + + 'TalsecRuntime SPM embed step.'); + } + else { + const snippet = [ + '', + ` ${FREERASP_SPM_EMBED_TAG}`, + " require Pod::Executable.execute_command('node', ['-p',", + ` 'require.resolve("freerasp-react-native/freerasp_spm.rb", {paths: [process.argv[1]]})',`, + ' __dir__]).strip', + ' freerasp_embed_talsec_spm!(installer)', + ].join('\n'); + const insertAt = anchorIndex + anchor.length; + contents = + contents.slice(0, insertAt) + snippet + contents.slice(insertAt); + (0, fs_1.writeFileSync)(podfilePath, contents); + } + } + return cfg; + }, + ]); +}; +const withRnTalsecIos = (config) => { + config = withFreeraspIosDynamicFrameworks(config); + config = withFreeraspIosSpmEmbed(config); + return config; +}; const withRnTalsecApp = (config, props) => { config = withBuildscriptDependency(config); config = withAndroidMinSdkVersion(config, props); config = withAndroidR8Version(config, props); + config = withRnTalsecIos(config); return config; }; let pkg = { diff --git a/plugin/src/index.ts b/plugin/src/index.ts index c3e8bb3..8025fd9 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -2,10 +2,14 @@ import { AndroidConfig, WarningAggregator, createRunOncePlugin, + withDangerousMod, + withPodfileProperties, withProjectBuildGradle, type ConfigPlugin, } from '@expo/config-plugins'; import { type ExpoConfig } from '@expo/config-types'; +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; import { type PluginConfigType } from './pluginConfig'; const { createBuildGradlePropsConfigPlugin } = AndroidConfig.BuildProperties; @@ -106,10 +110,74 @@ const withAndroidR8Version: ConfigPlugin = ( }); }; +// iOS — SPM delivery of TalsecRuntime (default on RN >= 0.75). Sets dynamic frameworks +// and injects a guarded TalsecRuntime embed into the Podfile post_install (skipped on the +// vendored fallback / FREERASP_DISABLE_SPM=1). Note: the Expo prebuild flow is less +// battle-tested than bare React Native. + +const FREERASP_SPM_EMBED_TAG = '# @generated freerasp-react-native (SPM embed)'; + +/** + * Force dynamically linked frameworks — required by `spm_dependency`. + */ +const withFreeraspIosDynamicFrameworks: ConfigPlugin = (config) => { + return withPodfileProperties(config, (cfg) => { + cfg.modResults['ios.useFrameworks'] = 'dynamic'; + return cfg; + }); +}; + +/** + * Inject the TalsecRuntime embed step into the generated Podfile `post_install`, + * so the SPM binary framework ends up in the app bundle (otherwise dyld fails at launch). + */ +const withFreeraspIosSpmEmbed: ConfigPlugin = (config) => { + return withDangerousMod(config, [ + 'ios', + (cfg) => { + const podfilePath = join(cfg.modRequest.platformProjectRoot, 'Podfile'); + let contents = readFileSync(podfilePath, 'utf-8'); + + if (!contents.includes(FREERASP_SPM_EMBED_TAG)) { + const anchor = 'post_install do |installer|'; + const anchorIndex = contents.indexOf(anchor); + if (anchorIndex === -1) { + WarningAggregator.addWarningIOS( + 'freerasp-react-native', + 'Could not find a `post_install` block in the Podfile to inject the ' + + 'TalsecRuntime SPM embed step.' + ); + } else { + const snippet = [ + '', + ` ${FREERASP_SPM_EMBED_TAG}`, + " require Pod::Executable.execute_command('node', ['-p',", + ` 'require.resolve("freerasp-react-native/freerasp_spm.rb", {paths: [process.argv[1]]})',`, + ' __dir__]).strip', + ' freerasp_embed_talsec_spm!(installer)', + ].join('\n'); + const insertAt = anchorIndex + anchor.length; + contents = + contents.slice(0, insertAt) + snippet + contents.slice(insertAt); + writeFileSync(podfilePath, contents); + } + } + return cfg; + }, + ]); +}; + +const withRnTalsecIos: ConfigPlugin = (config) => { + config = withFreeraspIosDynamicFrameworks(config); + config = withFreeraspIosSpmEmbed(config); + return config; +}; + const withRnTalsecApp: ConfigPlugin = (config, props) => { config = withBuildscriptDependency(config); config = withAndroidMinSdkVersion(config, props); config = withAndroidR8Version(config, props); + config = withRnTalsecIos(config); return config; };