From 59318cef419e0d8bbf84a4fc33d35091bf04503c Mon Sep 17 00:00:00 2001 From: martinzigrai Date: Tue, 14 Jul 2026 12:37:30 +0200 Subject: [PATCH 01/15] feat: add opt-in SPM integration for TalsecRuntime on iOS --- example/ios/Podfile | 4 ++++ freerasp-react-native.podspec | 42 ++++++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/example/ios/Podfile b/example/ios/Podfile index e038f35..d2f40b2 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -8,6 +8,10 @@ require Pod::Executable.execute_command('node', ['-p', platform :ios, min_ios_version_supported prepare_react_native_project! +# To test the experimental Swift Package Manager integration of freerasp-react-native, +# install with the SPM opt-in flag and dynamic frameworks: +# FREERASP_USE_SPM=1 USE_FRAMEWORKS=dynamic pod install +# Without these flags the default vendored xcframework is used. linkage = ENV['USE_FRAMEWORKS'] if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green diff --git a/freerasp-react-native.podspec b/freerasp-react-native.podspec index 9d17785..935ffbc 100644 --- a/freerasp-react-native.podspec +++ b/freerasp-react-native.podspec @@ -14,14 +14,40 @@ 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" - + # Opt-in Swift Package Manager integration for the TalsecRuntime dependency. + # Enabled only when FREERASP_USE_SPM=1 AND the running React Native provides the + # spm_dependency helper (RN >= 0.75). Otherwise the vendored xcframework is used + # (default — no breaking change for existing consumers). The SPM path additionally + # requires USE_FRAMEWORKS=dynamic in the consumer Podfile and iOS 13+. + use_spm = ENV['FREERASP_USE_SPM'] == '1' && respond_to?(:spm_dependency, true) + + 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 + # TalsecRuntime is resolved via Swift Package Manager (talsec/Free-RASP-iOS). + # spm_dependency injects an SPM reference into the Pods-generated Xcode project. + # NOTE: the vendored xcframework is intentionally NOT linked here to avoid + # duplicate symbols. + s.ios.deployment_target = '13.0' + spm_dependency(s, + url: 'https://github.com/talsec/Free-RASP-iOS', + requirement: { kind: 'upToNextMajorVersion', minimumVersion: '6.14.5' }, + products: ['TalsecRuntime'] + ) + else + # TalsecRuntime is provided by the vendored xcframework (default). + 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) From f6c7a5867c4b4a12eaacc32d12f5398764163e9f Mon Sep 17 00:00:00 2001 From: martinzigrai Date: Tue, 14 Jul 2026 12:37:30 +0200 Subject: [PATCH 02/15] ci: add non-blocking iOS SPM smoke-test job --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5cd143..8b5b7a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,3 +99,29 @@ jobs: working-directory: ./example run: | yarn build:ios + + build-ios-spm: + # Experimental: smoke-test the opt-in Swift Package Manager integration + # (FREERASP_USE_SPM=1). Non-blocking while SPM support is experimental; the + # vendored build-ios job above remains the gating check. + runs-on: macos-latest + needs: build-library + continue-on-error: true + env: + FREERASP_USE_SPM: "1" + 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 From 8ee8c1f9e6e361fdc07d7627f502670168a12a59 Mon Sep 17 00:00:00 2001 From: martinzigrai Date: Thu, 16 Jul 2026 12:59:51 +0200 Subject: [PATCH 03/15] feat: add TalsecRuntime SPM embed helper and placeholder wiring --- example/ios/Podfile | 12 +++++++ freerasp-react-native.podspec | 32 +++++++++++++++-- freerasp_spm.rb | 66 +++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 freerasp_spm.rb diff --git a/example/ios/Podfile b/example/ios/Podfile index d2f40b2..034c04e 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -35,5 +35,17 @@ target 'FreeraspRNExample' do :mac_catalyst_enabled => false, # :ccache_enabled => true ) + + # freeRASP: on the opt-in SPM path, embed TalsecRuntime into the app target + # so it ships inside the app bundle (otherwise dyld fails at launch). + # In a real app (freerasp-react-native installed in node_modules) resolve it via: + # require Pod::Executable.execute_command('node', ['-p', + # 'require.resolve("freerasp-react-native/freerasp_spm.rb", {paths: [process.argv[1]]})', + # __dir__]).strip + # This example consumes the library from the repo root, so it requires it directly. + if ENV['FREERASP_USE_SPM'] == '1' + require_relative '../../freerasp_spm.rb' + freerasp_embed_talsec_spm!(installer) + end end end diff --git a/freerasp-react-native.podspec b/freerasp-react-native.podspec index 935ffbc..4dd10f1 100644 --- a/freerasp-react-native.podspec +++ b/freerasp-react-native.podspec @@ -3,6 +3,31 @@ 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' +# --------------------------------------------------------------------------- +# Swift Package Manager (SPM) — PHASE 1 PRE-PREPARATION (non-breaking) +# +# TODO(SPM infra): the dedicated RN-flavour SPM manifest repo + GCP-hosted +# TalsecRuntime xcframework are NOT ready yet. The values below are PLACEHOLDERS. +# The opt-in SPM path (FREERASP_USE_SPM=1) is wired but NOT usable until the infra +# lands. The vendored xcframework remains the default and keeps working. +# +# IMPORTANT: this must point at the RN-flavour manifest repo — NOT talsec/Free-RASP-iOS +# (that is the *native* iOS flavour and is the wrong artifact for React Native). +# +# PHASE 2 FLIP (do this once the infra is ready — breaking, major version): +# 1. Set TALSEC_SPM_URL / TALSEC_SPM_MIN_VERSION to the real manifest repo + tag. +# 2. Remove the `use_spm` flag and the vendored branch below; call spm_dependency +# unconditionally; `raise` if spm_dependency is unavailable (RN < 0.75). +# 3. `git rm ios/TalsecRuntime.xcframework` and drop its ref in the .xcodeproj. +# 4. example/ios/Podfile: embed unconditionally + dynamic frameworks. +# 5. Expo plugin: enable iOS SPM by default (see plugin/src/index.ts). +# 6. package.json: major bump + react-native peerDependency ">=0.75.0". +# 7. CI: make the SPM iOS build the blocking gate; drop the vendored job. +# 8. Release: drop dSYM check/attach (dSYMs come from the GCP/upstream flow). +# --------------------------------------------------------------------------- +talsec_spm_url = 'https://github.com/talsec/TODO-freerasp-ios-spm' # TODO(SPM infra): real manifest repo +talsec_spm_min_version = '0.0.0' # TODO(SPM infra): real version once the manifest repo is tagged + Pod::Spec.new do |s| s.name = "freerasp-react-native" s.version = package["version"] @@ -29,14 +54,15 @@ Pod::Spec.new do |s| ] if use_spm - # TalsecRuntime is resolved via Swift Package Manager (talsec/Free-RASP-iOS). + # TalsecRuntime is resolved via Swift Package Manager (dedicated RN-flavour manifest repo). # spm_dependency injects an SPM reference into the Pods-generated Xcode project. # NOTE: the vendored xcframework is intentionally NOT linked here to avoid # duplicate symbols. + # TODO(SPM infra): placeholders above — this branch is not usable until the infra lands. s.ios.deployment_target = '13.0' spm_dependency(s, - url: 'https://github.com/talsec/Free-RASP-iOS', - requirement: { kind: 'upToNextMajorVersion', minimumVersion: '6.14.5' }, + url: talsec_spm_url, + requirement: { kind: 'upToNextMajorVersion', minimumVersion: talsec_spm_min_version }, products: ['TalsecRuntime'] ) else diff --git a/freerasp_spm.rb b/freerasp_spm.rb new file mode 100644 index 0000000..f824d5c --- /dev/null +++ b/freerasp_spm.rb @@ -0,0 +1,66 @@ +# freeRASP — Swift Package Manager (SPM) opt-in helper. +# +# When the opt-in SPM integration is enabled (FREERASP_USE_SPM=1), the podspec +# resolves TalsecRuntime through the `spm_dependency` helper, which attaches the +# package product to the *pod* target only. With dynamically linked frameworks +# (USE_FRAMEWORKS=dynamic) the app then crashes at launch with: +# +# Library not loaded: @rpath/TalsecRuntime.framework/TalsecRuntime +# +# because nothing embeds the Swift package's binary framework into the app bundle. +# +# Call `freerasp_embed_talsec_spm!(installer)` from your Podfile `post_install` +# (after `react_native_post_install`) to add TalsecRuntime to the application +# target(s) so Xcode embeds and signs it automatically. +# +# Keep the version requirement in sync with `freerasp-react-native.podspec`. +# +# TODO(SPM infra): the `url`/`requirement` defaults below are PLACEHOLDERS. They must +# match `freerasp-react-native.podspec` and point at the dedicated RN-flavour manifest +# repo (NOT talsec/Free-RASP-iOS, which is the native flavour). Not usable until the +# GCP-hosted xcframework + manifest repo infra is ready. + +def freerasp_embed_talsec_spm!(installer, + url: 'https://github.com/talsec/TODO-freerasp-ios-spm', + requirement: { kind: 'upToNextMajorVersion', minimumVersion: '0.0.0' }, + product: 'TalsecRuntime') + + pkg_class = Xcodeproj::Project::Object::XCRemoteSwiftPackageReference + ref_class = Xcodeproj::Project::Object::XCSwiftPackageProductDependency + + installer.aggregate_targets.each do |aggregate_target| + project = aggregate_target.user_project + next if project.nil? + + # Find or create the package reference in the application project. + pkg = project.root_object.package_references.find do |p| + p.class == pkg_class && p.repositoryURL == url + end + unless pkg + pkg = project.new(pkg_class) + pkg.repositoryURL = url + pkg.requirement = requirement + project.root_object.package_references << pkg + end + + aggregate_target.user_targets.each do |target| + next unless target.respond_to?(:product_type) && + target.product_type == 'com.apple.product-type.application' + + # Add the product dependency to the app target; Xcode embeds & signs + # Swift package framework products of an application target automatically. + existing = target.package_product_dependencies.find do |r| + r.class == ref_class && r.package == pkg && r.product_name == product + end + next if existing + + 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 + + project.save + end +end From c4e153261395a2e3d9e1b4e19b73cd50c80863ac Mon Sep 17 00:00:00 2001 From: martinzigrai Date: Thu, 16 Jul 2026 12:59:51 +0200 Subject: [PATCH 04/15] feat: scaffold opt-in Expo iOS SPM support --- plugin/build/index.d.ts | 1 + plugin/build/index.js | 90 ++++++++++++++++++++++++++++++++++ plugin/build/pluginConfig.d.ts | 23 +++++++++ plugin/src/index.ts | 80 ++++++++++++++++++++++++++++++ plugin/src/pluginConfig.ts | 24 +++++++++ 5 files changed, 218 insertions(+) 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..549ffd7 100644 --- a/plugin/build/index.js +++ b/plugin/build/index.js @@ -1,6 +1,31 @@ "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; Object.defineProperty(exports, "__esModule", { value: true }); const config_plugins_1 = require("@expo/config-plugins"); +const fs = __importStar(require("fs")); +const path = __importStar(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 +96,75 @@ const withAndroidR8Version = (expoConfig, props) => { return config; }); }; +// --------------------------------------------------------------------------- +// iOS — experimental Swift Package Manager delivery of TalsecRuntime (opt-in). +// +// TODO(SPM infra): NOT usable until the dedicated RN-flavour manifest repo + +// GCP-hosted xcframework are ready. Enabled only via `ios.useSpm: true`; off by +// default so existing Expo apps keep using the vendored xcframework (non-breaking). +// TODO(verify): the Podfile anchor and the full `expo prebuild` flow are unverified +// until the SPM infra lands (see freerasp-react-native.podspec "PHASE 2 FLIP"). +// --------------------------------------------------------------------------- +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, (config) => { + config.modResults['ios.useFrameworks'] = 'dynamic'; + return config; + }); +}; +/** + * 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', + (config) => { + const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile'); + let contents = fs.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); + fs.writeFileSync(podfilePath, contents); + } + } + return config; + }, + ]); +}; +const withRnTalsecIos = (config, props) => { + // Off by default (non-breaking). Enable via `ios.useSpm: true`. + if (!props?.ios?.useSpm) { + return 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, props); return config; }; let pkg = { diff --git a/plugin/build/pluginConfig.d.ts b/plugin/build/pluginConfig.d.ts index 3121ba3..9a77c38 100644 --- a/plugin/build/pluginConfig.d.ts +++ b/plugin/build/pluginConfig.d.ts @@ -7,6 +7,11 @@ export interface PluginConfigType { * @platform android */ android?: PluginConfigTypeAndroid; + /** + * Interface representing available configuration for iOS native build. + * @platform ios + */ + ios?: PluginConfigTypeIos; } /** * Interface representing available configuration for Android native build properties. @@ -19,3 +24,21 @@ export interface PluginConfigTypeAndroid { minSdkVersion?: number; R8Version?: string; } +/** + * Interface representing available configuration for iOS native build. + * @platform ios + * + * TODO(SPM infra): experimental Swift Package Manager delivery of TalsecRuntime. + * Not usable until the dedicated RN-flavour manifest repo + GCP-hosted xcframework + * are ready. Defaults to off — the vendored xcframework is used. + */ +export interface PluginConfigTypeIos { + /** + * Opt in to the experimental SPM integration of TalsecRuntime. When enabled the + * plugin sets dynamically linked frameworks and injects the TalsecRuntime embed + * step into the generated Podfile's `post_install`. + * + * @default false + */ + useSpm?: boolean; +} diff --git a/plugin/src/index.ts b/plugin/src/index.ts index c3e8bb3..bbee851 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 * as fs from 'fs'; +import * as path from 'path'; import { type PluginConfigType } from './pluginConfig'; const { createBuildGradlePropsConfigPlugin } = AndroidConfig.BuildProperties; @@ -106,10 +110,86 @@ const withAndroidR8Version: ConfigPlugin = ( }); }; +// --------------------------------------------------------------------------- +// iOS — experimental Swift Package Manager delivery of TalsecRuntime (opt-in). +// +// TODO(SPM infra): NOT usable until the dedicated RN-flavour manifest repo + +// GCP-hosted xcframework are ready. Enabled only via `ios.useSpm: true`; off by +// default so existing Expo apps keep using the vendored xcframework (non-breaking). +// TODO(verify): the Podfile anchor and the full `expo prebuild` flow are unverified +// until the SPM infra lands (see freerasp-react-native.podspec "PHASE 2 FLIP"). +// --------------------------------------------------------------------------- + +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, (config) => { + config.modResults['ios.useFrameworks'] = 'dynamic'; + return config; + }); +}; + +/** + * 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', + (config) => { + const podfilePath = path.join( + config.modRequest.platformProjectRoot, + 'Podfile' + ); + let contents = fs.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); + fs.writeFileSync(podfilePath, contents); + } + } + return config; + }, + ]); +}; + +const withRnTalsecIos: ConfigPlugin = (config, props) => { + // Off by default (non-breaking). Enable via `ios.useSpm: true`. + if (!props?.ios?.useSpm) { + return 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, props); return config; }; diff --git a/plugin/src/pluginConfig.ts b/plugin/src/pluginConfig.ts index 075ef0f..97c0108 100644 --- a/plugin/src/pluginConfig.ts +++ b/plugin/src/pluginConfig.ts @@ -7,6 +7,11 @@ export interface PluginConfigType { * @platform android */ android?: PluginConfigTypeAndroid; + /** + * Interface representing available configuration for iOS native build. + * @platform ios + */ + ios?: PluginConfigTypeIos; } /** @@ -20,3 +25,22 @@ export interface PluginConfigTypeAndroid { minSdkVersion?: number; R8Version?: string; } + +/** + * Interface representing available configuration for iOS native build. + * @platform ios + * + * TODO(SPM infra): experimental Swift Package Manager delivery of TalsecRuntime. + * Not usable until the dedicated RN-flavour manifest repo + GCP-hosted xcframework + * are ready. Defaults to off — the vendored xcframework is used. + */ +export interface PluginConfigTypeIos { + /** + * Opt in to the experimental SPM integration of TalsecRuntime. When enabled the + * plugin sets dynamically linked frameworks and injects the TalsecRuntime embed + * step into the generated Podfile's `post_install`. + * + * @default false + */ + useSpm?: boolean; +} From 5bc2690ef0531cd5ba74480589a5e6cf9015aa82 Mon Sep 17 00:00:00 2001 From: martinzigrai Date: Thu, 16 Jul 2026 14:04:04 +0200 Subject: [PATCH 05/15] feat: make SPM the sole iOS delivery for TalsecRuntime (stage 2 prep) --- example/ios/Podfile | 25 +++++------ freerasp-react-native.podspec | 79 +++++++++++++---------------------- package.json | 2 +- 3 files changed, 39 insertions(+), 67 deletions(-) diff --git a/example/ios/Podfile b/example/ios/Podfile index 034c04e..bb52884 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -8,15 +8,12 @@ require Pod::Executable.execute_command('node', ['-p', platform :ios, min_ios_version_supported prepare_react_native_project! -# To test the experimental Swift Package Manager integration of freerasp-react-native, -# install with the SPM opt-in flag and dynamic frameworks: -# FREERASP_USE_SPM=1 USE_FRAMEWORKS=dynamic pod install -# Without these flags the default vendored xcframework is used. -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! @@ -36,16 +33,14 @@ target 'FreeraspRNExample' do # :ccache_enabled => true ) - # freeRASP: on the opt-in SPM path, embed TalsecRuntime into the app target - # so it ships inside the app bundle (otherwise dyld fails at launch). + # freeRASP: embed TalsecRuntime (delivered via SPM) into the app target so it + # ships inside the app bundle (otherwise dyld fails at launch). # In a real app (freerasp-react-native installed in node_modules) resolve it via: # require Pod::Executable.execute_command('node', ['-p', # 'require.resolve("freerasp-react-native/freerasp_spm.rb", {paths: [process.argv[1]]})', # __dir__]).strip # This example consumes the library from the repo root, so it requires it directly. - if ENV['FREERASP_USE_SPM'] == '1' - require_relative '../../freerasp_spm.rb' - freerasp_embed_talsec_spm!(installer) - end + 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 4dd10f1..9e44c82 100644 --- a/freerasp-react-native.podspec +++ b/freerasp-react-native.podspec @@ -4,26 +4,20 @@ 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' # --------------------------------------------------------------------------- -# Swift Package Manager (SPM) — PHASE 1 PRE-PREPARATION (non-breaking) +# Swift Package Manager (SPM) delivery of TalsecRuntime — STAGE 2 SHAPE. # -# TODO(SPM infra): the dedicated RN-flavour SPM manifest repo + GCP-hosted -# TalsecRuntime xcframework are NOT ready yet. The values below are PLACEHOLDERS. -# The opt-in SPM path (FREERASP_USE_SPM=1) is wired but NOT usable until the infra -# lands. The vendored xcframework remains the default and keeps working. +# TalsecRuntime is delivered exclusively via SPM (a dedicated RN-flavour manifest +# repo + a GCP-hosted xcframework). CocoaPods stays for React Native itself; +# `spm_dependency` (RN >= 0.75) injects the SPM reference into the Pods project and +# `freerasp_embed_talsec_spm!` (see freerasp_spm.rb) embeds the framework into the +# app target. Requires USE_FRAMEWORKS=dynamic in the consumer Podfile and iOS 13+. # -# IMPORTANT: this must point at the RN-flavour manifest repo — NOT talsec/Free-RASP-iOS -# (that is the *native* iOS flavour and is the wrong artifact for React Native). -# -# PHASE 2 FLIP (do this once the infra is ready — breaking, major version): -# 1. Set TALSEC_SPM_URL / TALSEC_SPM_MIN_VERSION to the real manifest repo + tag. -# 2. Remove the `use_spm` flag and the vendored branch below; call spm_dependency -# unconditionally; `raise` if spm_dependency is unavailable (RN < 0.75). -# 3. `git rm ios/TalsecRuntime.xcframework` and drop its ref in the .xcodeproj. -# 4. example/ios/Podfile: embed unconditionally + dynamic frameworks. -# 5. Expo plugin: enable iOS SPM by default (see plugin/src/index.ts). -# 6. package.json: major bump + react-native peerDependency ">=0.75.0". -# 7. CI: make the SPM iOS build the blocking gate; drop the vendored job. -# 8. Release: drop dSYM check/attach (dSYMs come from the GCP/upstream flow). +# TODO(SPM infra): the manifest repo + GCP-hosted xcframework are NOT ready yet, so +# the values below are PLACEHOLDERS — the iOS build will not resolve until they exist. +# Before releasing, also: `git rm ios/TalsecRuntime.xcframework` + exclude it from npm, +# major version bump, and drop the dSYM check/attach release steps. +# IMPORTANT: point at the RN-flavour manifest repo — NOT talsec/Free-RASP-iOS +# (that is the native flavour and the wrong artifact for React Native). # --------------------------------------------------------------------------- talsec_spm_url = 'https://github.com/talsec/TODO-freerasp-ios-spm' # TODO(SPM infra): real manifest repo talsec_spm_min_version = '0.0.0' # TODO(SPM infra): real version once the manifest repo is tagged @@ -36,43 +30,26 @@ Pod::Spec.new do |s| s.license = package["license"] s.authors = package["author"] - s.platforms = { :ios => "11.0" } + s.platforms = { :ios => "13.0" } s.source = { :git => "https://github.com/talsec/freerasp-react-native.git", :tag => "#{s.version}" } - # Opt-in Swift Package Manager integration for the TalsecRuntime dependency. - # Enabled only when FREERASP_USE_SPM=1 AND the running React Native provides the - # spm_dependency helper (RN >= 0.75). Otherwise the vendored xcframework is used - # (default — no breaking change for existing consumers). The SPM path additionally - # requires USE_FRAMEWORKS=dynamic in the consumer Podfile and iOS 13+. - use_spm = ENV['FREERASP_USE_SPM'] == '1' && respond_to?(:spm_dependency, true) - - 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}', - ] + 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}' - if use_spm - # TalsecRuntime is resolved via Swift Package Manager (dedicated RN-flavour manifest repo). - # spm_dependency injects an SPM reference into the Pods-generated Xcode project. - # NOTE: the vendored xcframework is intentionally NOT linked here to avoid - # duplicate symbols. - # TODO(SPM infra): placeholders above — this branch is not usable until the infra lands. - s.ios.deployment_target = '13.0' - spm_dependency(s, - url: talsec_spm_url, - requirement: { kind: 'upToNextMajorVersion', minimumVersion: talsec_spm_min_version }, - products: ['TalsecRuntime'] - ) - else - # TalsecRuntime is provided by the vendored xcframework (default). - source_globs << 'ios/TalsecRuntime.xcframework' - s.xcconfig = { 'OTHER_LDFLAGS' => '-framework TalsecRuntime' } - s.ios.vendored_frameworks = 'ios/TalsecRuntime.xcframework' + # TalsecRuntime is resolved exclusively via Swift Package Manager. spm_dependency + # (RN >= 0.75) injects the SPM reference into the Pods-generated Xcode project; the + # binary framework is embedded into the app target by `freerasp_embed_talsec_spm!` + # (freerasp_spm.rb), called from the consumer Podfile post_install. + unless respond_to?(:spm_dependency, true) + raise "[freerasp-react-native] iOS integration requires React Native >= 0.75 (the spm_dependency helper)." end - - s.source_files = source_globs + spm_dependency(s, + url: talsec_spm_url, + requirement: { kind: 'upToNextMajorVersion', minimumVersion: talsec_spm_min_version }, + products: ['TalsecRuntime'] + ) # 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. diff --git a/package.json b/package.json index ba22a47..4012a4a 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "peerDependencies": { "expo": ">=47.0.0", "react": "*", - "react-native": "*" + "react-native": ">=0.75.0" }, "peerDependenciesMeta": { "expo": { From 6d804b5348e1ac606e2da8c06a99e71e88595861 Mon Sep 17 00:00:00 2001 From: martinzigrai Date: Thu, 16 Jul 2026 14:04:04 +0200 Subject: [PATCH 06/15] feat: enable Expo iOS SPM support by default --- plugin/build/index.js | 17 +++++++---------- plugin/build/pluginConfig.d.ts | 23 ----------------------- plugin/src/index.ts | 17 +++++++---------- plugin/src/pluginConfig.ts | 24 ------------------------ 4 files changed, 14 insertions(+), 67 deletions(-) diff --git a/plugin/build/index.js b/plugin/build/index.js index 549ffd7..118c780 100644 --- a/plugin/build/index.js +++ b/plugin/build/index.js @@ -97,13 +97,14 @@ const withAndroidR8Version = (expoConfig, props) => { }); }; // --------------------------------------------------------------------------- -// iOS — experimental Swift Package Manager delivery of TalsecRuntime (opt-in). +// iOS — Swift Package Manager delivery of TalsecRuntime. // +// Sets dynamically linked frameworks (required by spm_dependency) and injects the +// TalsecRuntime embed step into the generated Podfile's post_install. // TODO(SPM infra): NOT usable until the dedicated RN-flavour manifest repo + -// GCP-hosted xcframework are ready. Enabled only via `ios.useSpm: true`; off by -// default so existing Expo apps keep using the vendored xcframework (non-breaking). +// GCP-hosted xcframework are ready (see freerasp-react-native.podspec). // TODO(verify): the Podfile anchor and the full `expo prebuild` flow are unverified -// until the SPM infra lands (see freerasp-react-native.podspec "PHASE 2 FLIP"). +// until the SPM infra lands. // --------------------------------------------------------------------------- const FREERASP_SPM_EMBED_TAG = '# @generated freerasp-react-native (SPM embed)'; /** @@ -151,11 +152,7 @@ const withFreeraspIosSpmEmbed = (config) => { }, ]); }; -const withRnTalsecIos = (config, props) => { - // Off by default (non-breaking). Enable via `ios.useSpm: true`. - if (!props?.ios?.useSpm) { - return config; - } +const withRnTalsecIos = (config) => { config = withFreeraspIosDynamicFrameworks(config); config = withFreeraspIosSpmEmbed(config); return config; @@ -164,7 +161,7 @@ const withRnTalsecApp = (config, props) => { config = withBuildscriptDependency(config); config = withAndroidMinSdkVersion(config, props); config = withAndroidR8Version(config, props); - config = withRnTalsecIos(config, props); + config = withRnTalsecIos(config); return config; }; let pkg = { diff --git a/plugin/build/pluginConfig.d.ts b/plugin/build/pluginConfig.d.ts index 9a77c38..3121ba3 100644 --- a/plugin/build/pluginConfig.d.ts +++ b/plugin/build/pluginConfig.d.ts @@ -7,11 +7,6 @@ export interface PluginConfigType { * @platform android */ android?: PluginConfigTypeAndroid; - /** - * Interface representing available configuration for iOS native build. - * @platform ios - */ - ios?: PluginConfigTypeIos; } /** * Interface representing available configuration for Android native build properties. @@ -24,21 +19,3 @@ export interface PluginConfigTypeAndroid { minSdkVersion?: number; R8Version?: string; } -/** - * Interface representing available configuration for iOS native build. - * @platform ios - * - * TODO(SPM infra): experimental Swift Package Manager delivery of TalsecRuntime. - * Not usable until the dedicated RN-flavour manifest repo + GCP-hosted xcframework - * are ready. Defaults to off — the vendored xcframework is used. - */ -export interface PluginConfigTypeIos { - /** - * Opt in to the experimental SPM integration of TalsecRuntime. When enabled the - * plugin sets dynamically linked frameworks and injects the TalsecRuntime embed - * step into the generated Podfile's `post_install`. - * - * @default false - */ - useSpm?: boolean; -} diff --git a/plugin/src/index.ts b/plugin/src/index.ts index bbee851..b58967c 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -111,13 +111,14 @@ const withAndroidR8Version: ConfigPlugin = ( }; // --------------------------------------------------------------------------- -// iOS — experimental Swift Package Manager delivery of TalsecRuntime (opt-in). +// iOS — Swift Package Manager delivery of TalsecRuntime. // +// Sets dynamically linked frameworks (required by spm_dependency) and injects the +// TalsecRuntime embed step into the generated Podfile's post_install. // TODO(SPM infra): NOT usable until the dedicated RN-flavour manifest repo + -// GCP-hosted xcframework are ready. Enabled only via `ios.useSpm: true`; off by -// default so existing Expo apps keep using the vendored xcframework (non-breaking). +// GCP-hosted xcframework are ready (see freerasp-react-native.podspec). // TODO(verify): the Podfile anchor and the full `expo prebuild` flow are unverified -// until the SPM infra lands (see freerasp-react-native.podspec "PHASE 2 FLIP"). +// until the SPM infra lands. // --------------------------------------------------------------------------- const FREERASP_SPM_EMBED_TAG = '# @generated freerasp-react-native (SPM embed)'; @@ -175,11 +176,7 @@ const withFreeraspIosSpmEmbed: ConfigPlugin = (config) => { ]); }; -const withRnTalsecIos: ConfigPlugin = (config, props) => { - // Off by default (non-breaking). Enable via `ios.useSpm: true`. - if (!props?.ios?.useSpm) { - return config; - } +const withRnTalsecIos: ConfigPlugin = (config) => { config = withFreeraspIosDynamicFrameworks(config); config = withFreeraspIosSpmEmbed(config); return config; @@ -189,7 +186,7 @@ const withRnTalsecApp: ConfigPlugin = (config, props) => { config = withBuildscriptDependency(config); config = withAndroidMinSdkVersion(config, props); config = withAndroidR8Version(config, props); - config = withRnTalsecIos(config, props); + config = withRnTalsecIos(config); return config; }; diff --git a/plugin/src/pluginConfig.ts b/plugin/src/pluginConfig.ts index 97c0108..075ef0f 100644 --- a/plugin/src/pluginConfig.ts +++ b/plugin/src/pluginConfig.ts @@ -7,11 +7,6 @@ export interface PluginConfigType { * @platform android */ android?: PluginConfigTypeAndroid; - /** - * Interface representing available configuration for iOS native build. - * @platform ios - */ - ios?: PluginConfigTypeIos; } /** @@ -25,22 +20,3 @@ export interface PluginConfigTypeAndroid { minSdkVersion?: number; R8Version?: string; } - -/** - * Interface representing available configuration for iOS native build. - * @platform ios - * - * TODO(SPM infra): experimental Swift Package Manager delivery of TalsecRuntime. - * Not usable until the dedicated RN-flavour manifest repo + GCP-hosted xcframework - * are ready. Defaults to off — the vendored xcframework is used. - */ -export interface PluginConfigTypeIos { - /** - * Opt in to the experimental SPM integration of TalsecRuntime. When enabled the - * plugin sets dynamically linked frameworks and injects the TalsecRuntime embed - * step into the generated Podfile's `post_install`. - * - * @default false - */ - useSpm?: boolean; -} From 383d91a35a2201b05a3d7df32870ea036c20b9a1 Mon Sep 17 00:00:00 2001 From: martinzigrai Date: Thu, 16 Jul 2026 14:04:04 +0200 Subject: [PATCH 07/15] ci: build iOS example via SPM only --- .github/workflows/ci.yml | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b5b7a3..23aceac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,33 +82,15 @@ jobs: yarn build:android build-ios: - runs-on: macos-latest - needs: build-library - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup - uses: ./.github/actions/setup - - - name: Install pods - working-directory: ./example/ios - run: pod install - - - name: Build example for iOS - working-directory: ./example - run: | - yarn build:ios - - build-ios-spm: - # Experimental: smoke-test the opt-in Swift Package Manager integration - # (FREERASP_USE_SPM=1). Non-blocking while SPM support is experimental; the - # vendored build-ios job above remains the gating check. + # iOS build. TalsecRuntime is delivered via Swift Package Manager (dynamic + # frameworks). This builds green once the real registry link replaces the + # placeholder in freerasp-react-native.podspec. + # Kept non-blocking (continue-on-error) only until the SPM infra lands; drop + # that line to make it a required gate. runs-on: macos-latest needs: build-library continue-on-error: true env: - FREERASP_USE_SPM: "1" USE_FRAMEWORKS: dynamic steps: - name: Checkout @@ -117,11 +99,11 @@ jobs: - name: Setup uses: ./.github/actions/setup - - name: Install pods (SPM) + - name: Install pods working-directory: ./example/ios run: pod install - - name: Build example for iOS (SPM) + - name: Build example for iOS working-directory: ./example run: | yarn build:ios From 80ed39320ed953c02eacdd26e68ab598656ddfcf Mon Sep 17 00:00:00 2001 From: martinzigrai Date: Thu, 16 Jul 2026 14:48:34 +0200 Subject: [PATCH 08/15] refactor: default to SPM with vendored fallback (react-native-firebase pattern) --- example/ios/Podfile | 13 ++++--- freerasp-react-native.podspec | 70 +++++++++++++++++++++-------------- package.json | 2 +- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/example/ios/Podfile b/example/ios/Podfile index bb52884..d3ec9bb 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -33,14 +33,17 @@ target 'FreeraspRNExample' do # :ccache_enabled => true ) - # freeRASP: embed TalsecRuntime (delivered via SPM) into the app target so it - # ships inside the app bundle (otherwise dyld fails at launch). - # In a real app (freerasp-react-native installed in node_modules) resolve it via: + # freeRASP: when TalsecRuntime is delivered via SPM (the default on RN >= 0.75 + # unless FREERASP_DISABLE_SPM=1), embed it into the app target so it ships inside + # the app bundle (otherwise dyld fails at launch). Skipped on the vendored fallback. + # In a real app (freerasp-react-native installed in node_modules) resolve the helper via: # require Pod::Executable.execute_command('node', ['-p', # 'require.resolve("freerasp-react-native/freerasp_spm.rb", {paths: [process.argv[1]]})', # __dir__]).strip # This example consumes the library from the repo root, so it requires it directly. - require_relative '../../freerasp_spm.rb' - freerasp_embed_talsec_spm!(installer) + if respond_to?(:spm_dependency, true) && ENV['FREERASP_DISABLE_SPM'] != '1' + require_relative '../../freerasp_spm.rb' + freerasp_embed_talsec_spm!(installer) + end end end diff --git a/freerasp-react-native.podspec b/freerasp-react-native.podspec index 9e44c82..82f0336 100644 --- a/freerasp-react-native.podspec +++ b/freerasp-react-native.podspec @@ -4,22 +4,24 @@ 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' # --------------------------------------------------------------------------- -# Swift Package Manager (SPM) delivery of TalsecRuntime — STAGE 2 SHAPE. +# TalsecRuntime delivery — Swift Package Manager by default, vendored fallback. +# (Same decision logic as react-native-firebase, non-breaking.) # -# TalsecRuntime is delivered exclusively via SPM (a dedicated RN-flavour manifest -# repo + a GCP-hosted xcframework). CocoaPods stays for React Native itself; -# `spm_dependency` (RN >= 0.75) injects the SPM reference into the Pods project and -# `freerasp_embed_talsec_spm!` (see freerasp_spm.rb) embeds the framework into the -# app target. Requires USE_FRAMEWORKS=dynamic in the consumer Podfile and iOS 13+. +# spm_dependency available (RN >= 0.75) AND FREERASP_DISABLE_SPM != '1' +# -> SPM (dedicated RN-flavour manifest repo) +# otherwise (RN < 0.75, or FREERASP_DISABLE_SPM=1) +# -> vendored TalsecRuntime.xcframework # -# TODO(SPM infra): the manifest repo + GCP-hosted xcframework are NOT ready yet, so -# the values below are PLACEHOLDERS — the iOS build will not resolve until they exist. -# Before releasing, also: `git rm ios/TalsecRuntime.xcframework` + exclude it from npm, -# major version bump, and drop the dSYM check/attach release steps. +# The SPM path requires USE_FRAMEWORKS=dynamic and iOS 13+. The vendored fallback +# keeps working on any RN / linkage, so this is not a breaking change. +# +# TODO(SPM infra): the dedicated RN-flavour manifest repo (which hosts the GCS-backed +# xcframework via binaryTarget) is NOT ready yet, so the values below are PLACEHOLDERS +# — the SPM path won't resolve until it exists; consumers fall back to vendored. # IMPORTANT: point at the RN-flavour manifest repo — NOT talsec/Free-RASP-iOS # (that is the native flavour and the wrong artifact for React Native). # --------------------------------------------------------------------------- -talsec_spm_url = 'https://github.com/talsec/TODO-freerasp-ios-spm' # TODO(SPM infra): real manifest repo +talsec_spm_url = 'https://github.com/talsec/TODO-freerasp-ios-spm' # TODO(SPM infra): real manifest repo git URL talsec_spm_min_version = '0.0.0' # TODO(SPM infra): real version once the manifest repo is tagged Pod::Spec.new do |s| @@ -30,26 +32,40 @@ Pod::Spec.new do |s| s.license = package["license"] s.authors = package["author"] - s.platforms = { :ios => "13.0" } + 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}' + # 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}', + ] - # TalsecRuntime is resolved exclusively via Swift Package Manager. spm_dependency - # (RN >= 0.75) injects the SPM reference into the Pods-generated Xcode project; the - # binary framework is embedded into the app target by `freerasp_embed_talsec_spm!` - # (freerasp_spm.rb), called from the consumer Podfile post_install. - unless respond_to?(:spm_dependency, true) - raise "[freerasp-react-native] iOS integration requires React Native >= 0.75 (the spm_dependency helper)." + if use_spm + # TalsecRuntime resolved via Swift Package Manager (dedicated RN-flavour manifest repo). + # spm_dependency injects the SPM reference into the Pods project; the framework is + # embedded into the app target by `freerasp_embed_talsec_spm!` (freerasp_spm.rb), + # called from the consumer Podfile post_install. + # TODO(SPM infra): placeholders above — this path is not usable until the infra lands. + s.ios.deployment_target = '13.0' + spm_dependency(s, + url: talsec_spm_url, + requirement: { kind: 'upToNextMajorVersion', minimumVersion: talsec_spm_min_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 - spm_dependency(s, - url: talsec_spm_url, - requirement: { kind: 'upToNextMajorVersion', minimumVersion: talsec_spm_min_version }, - products: ['TalsecRuntime'] - ) + + 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. diff --git a/package.json b/package.json index 4012a4a..ba22a47 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "peerDependencies": { "expo": ">=47.0.0", "react": "*", - "react-native": ">=0.75.0" + "react-native": "*" }, "peerDependenciesMeta": { "expo": { From e30a6b6af565f4d8d8e75992632f4ee13abe9de1 Mon Sep 17 00:00:00 2001 From: martinzigrai Date: Thu, 16 Jul 2026 14:48:34 +0200 Subject: [PATCH 09/15] fix: guard Expo iOS SPM embed on SPM-active condition --- plugin/build/index.js | 24 ++++++++++++++---------- plugin/src/index.ts | 24 ++++++++++++++---------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/plugin/build/index.js b/plugin/build/index.js index 118c780..fa7e836 100644 --- a/plugin/build/index.js +++ b/plugin/build/index.js @@ -97,14 +97,16 @@ const withAndroidR8Version = (expoConfig, props) => { }); }; // --------------------------------------------------------------------------- -// iOS — Swift Package Manager delivery of TalsecRuntime. +// iOS — Swift Package Manager delivery of TalsecRuntime (default on RN >= 0.75). // -// Sets dynamically linked frameworks (required by spm_dependency) and injects the -// TalsecRuntime embed step into the generated Podfile's post_install. +// Sets dynamically linked frameworks (required by spm_dependency) and injects a +// guarded TalsecRuntime embed step into the generated Podfile's post_install. The +// embed is skipped when SPM is unavailable or FREERASP_DISABLE_SPM=1 (vendored +// fallback), matching the podspec's decision logic. // TODO(SPM infra): NOT usable until the dedicated RN-flavour manifest repo + -// GCP-hosted xcframework are ready (see freerasp-react-native.podspec). -// TODO(verify): the Podfile anchor and the full `expo prebuild` flow are unverified -// until the SPM infra lands. +// GCS-hosted xcframework are ready (see freerasp-react-native.podspec). +// TODO(verify): the Podfile anchor, the FREERASP_DISABLE_SPM escape hatch, and the +// full `expo prebuild` flow are unverified until the SPM infra lands. // --------------------------------------------------------------------------- const FREERASP_SPM_EMBED_TAG = '# @generated freerasp-react-native (SPM embed)'; /** @@ -137,10 +139,12 @@ const withFreeraspIosSpmEmbed = (config) => { 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)', + " if respond_to?(:spm_dependency, true) && ENV['FREERASP_DISABLE_SPM'] != '1'", + " 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)', + ' end', ].join('\n'); const insertAt = anchorIndex + anchor.length; contents = diff --git a/plugin/src/index.ts b/plugin/src/index.ts index b58967c..47f0b12 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -111,14 +111,16 @@ const withAndroidR8Version: ConfigPlugin = ( }; // --------------------------------------------------------------------------- -// iOS — Swift Package Manager delivery of TalsecRuntime. +// iOS — Swift Package Manager delivery of TalsecRuntime (default on RN >= 0.75). // -// Sets dynamically linked frameworks (required by spm_dependency) and injects the -// TalsecRuntime embed step into the generated Podfile's post_install. +// Sets dynamically linked frameworks (required by spm_dependency) and injects a +// guarded TalsecRuntime embed step into the generated Podfile's post_install. The +// embed is skipped when SPM is unavailable or FREERASP_DISABLE_SPM=1 (vendored +// fallback), matching the podspec's decision logic. // TODO(SPM infra): NOT usable until the dedicated RN-flavour manifest repo + -// GCP-hosted xcframework are ready (see freerasp-react-native.podspec). -// TODO(verify): the Podfile anchor and the full `expo prebuild` flow are unverified -// until the SPM infra lands. +// GCS-hosted xcframework are ready (see freerasp-react-native.podspec). +// TODO(verify): the Podfile anchor, the FREERASP_DISABLE_SPM escape hatch, and the +// full `expo prebuild` flow are unverified until the SPM infra lands. // --------------------------------------------------------------------------- const FREERASP_SPM_EMBED_TAG = '# @generated freerasp-react-native (SPM embed)'; @@ -160,10 +162,12 @@ const withFreeraspIosSpmEmbed: ConfigPlugin = (config) => { 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)', + " if respond_to?(:spm_dependency, true) && ENV['FREERASP_DISABLE_SPM'] != '1'", + " 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)', + ' end', ].join('\n'); const insertAt = anchorIndex + anchor.length; contents = From 2e502d51cd5cdaa6d2d9288e486cc67286861436 Mon Sep 17 00:00:00 2001 From: martinzigrai Date: Thu, 16 Jul 2026 14:48:34 +0200 Subject: [PATCH 10/15] ci: add vendored fallback gate alongside non-blocking SPM build --- .github/workflows/ci.yml | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23aceac..1b7f2d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,16 +82,12 @@ jobs: yarn build:android build-ios: - # iOS build. TalsecRuntime is delivered via Swift Package Manager (dynamic - # frameworks). This builds green once the real registry link replaces the - # placeholder in freerasp-react-native.podspec. - # Kept non-blocking (continue-on-error) only until the SPM infra lands; drop - # that line to make it a required gate. + # Vendored TalsecRuntime fallback (FREERASP_DISABLE_SPM=1) — the required gate. + # Proves the non-breaking fallback keeps building while the SPM infra is not ready. runs-on: macos-latest needs: build-library - continue-on-error: true env: - USE_FRAMEWORKS: dynamic + FREERASP_DISABLE_SPM: "1" steps: - name: Checkout uses: actions/checkout@v6 @@ -107,3 +103,28 @@ jobs: working-directory: ./example run: | yarn build:ios + + build-ios-spm: + # SPM path (default on RN >= 0.75, dynamic frameworks). Non-blocking until the + # real manifest-repo/GCS link replaces the placeholder in the podspec; drop + # continue-on-error to make it a required gate once the SPM infra is live. + runs-on: macos-latest + needs: build-library + continue-on-error: true + 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 From 16543eeb2a106ec222a796bd755402af83c38b07 Mon Sep 17 00:00:00 2001 From: martinzigrai Date: Thu, 16 Jul 2026 16:10:09 +0200 Subject: [PATCH 11/15] feat: point iOS SPM at Free-RASP-ReactNative-SPM manifest repo (exact 6.14.4) --- .github/workflows/ci.yml | 8 ++------ freerasp-react-native.podspec | 34 ++++++++-------------------------- 2 files changed, 10 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b7f2d9..4836ead 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,8 +82,7 @@ jobs: yarn build:android build-ios: - # Vendored TalsecRuntime fallback (FREERASP_DISABLE_SPM=1) — the required gate. - # Proves the non-breaking fallback keeps building while the SPM infra is not ready. + # Vendored TalsecRuntime fallback (FREERASP_DISABLE_SPM=1). runs-on: macos-latest needs: build-library env: @@ -105,12 +104,9 @@ jobs: yarn build:ios build-ios-spm: - # SPM path (default on RN >= 0.75, dynamic frameworks). Non-blocking until the - # real manifest-repo/GCS link replaces the placeholder in the podspec; drop - # continue-on-error to make it a required gate once the SPM infra is live. + # SPM path (default, dynamic frameworks). runs-on: macos-latest needs: build-library - continue-on-error: true env: USE_FRAMEWORKS: dynamic steps: diff --git a/freerasp-react-native.podspec b/freerasp-react-native.podspec index 82f0336..2ffc722 100644 --- a/freerasp-react-native.podspec +++ b/freerasp-react-native.podspec @@ -3,26 +3,11 @@ 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 delivery — Swift Package Manager by default, vendored fallback. -# (Same decision logic as react-native-firebase, non-breaking.) -# -# spm_dependency available (RN >= 0.75) AND FREERASP_DISABLE_SPM != '1' -# -> SPM (dedicated RN-flavour manifest repo) -# otherwise (RN < 0.75, or FREERASP_DISABLE_SPM=1) -# -> vendored TalsecRuntime.xcframework -# -# The SPM path requires USE_FRAMEWORKS=dynamic and iOS 13+. The vendored fallback -# keeps working on any RN / linkage, so this is not a breaking change. -# -# TODO(SPM infra): the dedicated RN-flavour manifest repo (which hosts the GCS-backed -# xcframework via binaryTarget) is NOT ready yet, so the values below are PLACEHOLDERS -# — the SPM path won't resolve until it exists; consumers fall back to vendored. -# IMPORTANT: point at the RN-flavour manifest repo — NOT talsec/Free-RASP-iOS -# (that is the native flavour and the wrong artifact for React Native). -# --------------------------------------------------------------------------- -talsec_spm_url = 'https://github.com/talsec/TODO-freerasp-ios-spm' # TODO(SPM infra): real manifest repo git URL -talsec_spm_min_version = '0.0.0' # TODO(SPM infra): real version once the manifest repo is tagged +# 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" @@ -47,15 +32,12 @@ Pod::Spec.new do |s| ] if use_spm - # TalsecRuntime resolved via Swift Package Manager (dedicated RN-flavour manifest repo). - # spm_dependency injects the SPM reference into the Pods project; the framework is - # embedded into the app target by `freerasp_embed_talsec_spm!` (freerasp_spm.rb), - # called from the consumer Podfile post_install. - # TODO(SPM infra): placeholders above — this path is not usable until the infra lands. + # 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: 'upToNextMajorVersion', minimumVersion: talsec_spm_min_version }, + requirement: { kind: 'exactVersion', version: talsec_spm_version }, products: ['TalsecRuntime'] ) else From f49f4f78a5665ca92da56c41c8b7091c2eef0d14 Mon Sep 17 00:00:00 2001 From: martinzigrai Date: Thu, 16 Jul 2026 16:10:09 +0200 Subject: [PATCH 12/15] fix: make SPM embed helper idempotent to avoid duplicate framework embed --- example/ios/Podfile | 17 +++-------- freerasp_spm.rb | 71 ++++++++++++++++++------------------------- plugin/build/index.js | 26 +++++----------- plugin/src/index.ts | 26 +++++----------- 4 files changed, 50 insertions(+), 90 deletions(-) diff --git a/example/ios/Podfile b/example/ios/Podfile index d3ec9bb..8bce2a4 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -33,17 +33,10 @@ target 'FreeraspRNExample' do # :ccache_enabled => true ) - # freeRASP: when TalsecRuntime is delivered via SPM (the default on RN >= 0.75 - # unless FREERASP_DISABLE_SPM=1), embed it into the app target so it ships inside - # the app bundle (otherwise dyld fails at launch). Skipped on the vendored fallback. - # In a real app (freerasp-react-native installed in node_modules) resolve the helper via: - # require Pod::Executable.execute_command('node', ['-p', - # 'require.resolve("freerasp-react-native/freerasp_spm.rb", {paths: [process.argv[1]]})', - # __dir__]).strip - # This example consumes the library from the repo root, so it requires it directly. - if respond_to?(:spm_dependency, true) && ENV['FREERASP_DISABLE_SPM'] != '1' - require_relative '../../freerasp_spm.rb' - freerasp_embed_talsec_spm!(installer) - end + # 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_spm.rb b/freerasp_spm.rb index f824d5c..7ec91db 100644 --- a/freerasp_spm.rb +++ b/freerasp_spm.rb @@ -1,64 +1,51 @@ -# freeRASP — Swift Package Manager (SPM) opt-in helper. -# -# When the opt-in SPM integration is enabled (FREERASP_USE_SPM=1), the podspec -# resolves TalsecRuntime through the `spm_dependency` helper, which attaches the -# package product to the *pod* target only. With dynamically linked frameworks -# (USE_FRAMEWORKS=dynamic) the app then crashes at launch with: -# -# Library not loaded: @rpath/TalsecRuntime.framework/TalsecRuntime -# -# because nothing embeds the Swift package's binary framework into the app bundle. -# -# Call `freerasp_embed_talsec_spm!(installer)` from your Podfile `post_install` -# (after `react_native_post_install`) to add TalsecRuntime to the application -# target(s) so Xcode embeds and signs it automatically. -# -# Keep the version requirement in sync with `freerasp-react-native.podspec`. -# -# TODO(SPM infra): the `url`/`requirement` defaults below are PLACEHOLDERS. They must -# match `freerasp-react-native.podspec` and point at the dedicated RN-flavour manifest -# repo (NOT talsec/Free-RASP-iOS, which is the native flavour). Not usable until the -# GCP-hosted xcframework + manifest repo infra is ready. +# 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/TODO-freerasp-ios-spm', - requirement: { kind: 'upToNextMajorVersion', minimumVersion: '0.0.0' }, + 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? - # Find or create the package reference in the application project. - pkg = project.root_object.package_references.find do |p| + 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 - unless pkg + + if spm_active pkg = project.new(pkg_class) pkg.repositoryURL = url pkg.requirement = requirement project.root_object.package_references << pkg - end - aggregate_target.user_targets.each do |target| - next unless target.respond_to?(:product_type) && - target.product_type == 'com.apple.product-type.application' - - # Add the product dependency to the app target; Xcode embeds & signs - # Swift package framework products of an application target automatically. - existing = target.package_product_dependencies.find do |r| - r.class == ref_class && r.package == pkg && r.product_name == product + 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 - next if existing - - 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 project.save diff --git a/plugin/build/index.js b/plugin/build/index.js index fa7e836..ace016c 100644 --- a/plugin/build/index.js +++ b/plugin/build/index.js @@ -96,18 +96,10 @@ const withAndroidR8Version = (expoConfig, props) => { return config; }); }; -// --------------------------------------------------------------------------- -// iOS — Swift Package Manager delivery of TalsecRuntime (default on RN >= 0.75). -// -// Sets dynamically linked frameworks (required by spm_dependency) and injects a -// guarded TalsecRuntime embed step into the generated Podfile's post_install. The -// embed is skipped when SPM is unavailable or FREERASP_DISABLE_SPM=1 (vendored -// fallback), matching the podspec's decision logic. -// TODO(SPM infra): NOT usable until the dedicated RN-flavour manifest repo + -// GCS-hosted xcframework are ready (see freerasp-react-native.podspec). -// TODO(verify): the Podfile anchor, the FREERASP_DISABLE_SPM escape hatch, and the -// full `expo prebuild` flow are unverified until the SPM infra lands. -// --------------------------------------------------------------------------- +// 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`. @@ -139,12 +131,10 @@ const withFreeraspIosSpmEmbed = (config) => { const snippet = [ '', ` ${FREERASP_SPM_EMBED_TAG}`, - " if respond_to?(:spm_dependency, true) && ENV['FREERASP_DISABLE_SPM'] != '1'", - " 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)', - ' end', + " 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 = diff --git a/plugin/src/index.ts b/plugin/src/index.ts index 47f0b12..0c9e788 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -110,18 +110,10 @@ const withAndroidR8Version: ConfigPlugin = ( }); }; -// --------------------------------------------------------------------------- -// iOS — Swift Package Manager delivery of TalsecRuntime (default on RN >= 0.75). -// -// Sets dynamically linked frameworks (required by spm_dependency) and injects a -// guarded TalsecRuntime embed step into the generated Podfile's post_install. The -// embed is skipped when SPM is unavailable or FREERASP_DISABLE_SPM=1 (vendored -// fallback), matching the podspec's decision logic. -// TODO(SPM infra): NOT usable until the dedicated RN-flavour manifest repo + -// GCS-hosted xcframework are ready (see freerasp-react-native.podspec). -// TODO(verify): the Podfile anchor, the FREERASP_DISABLE_SPM escape hatch, and the -// full `expo prebuild` flow are unverified until the SPM infra lands. -// --------------------------------------------------------------------------- +// 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)'; @@ -162,12 +154,10 @@ const withFreeraspIosSpmEmbed: ConfigPlugin = (config) => { const snippet = [ '', ` ${FREERASP_SPM_EMBED_TAG}`, - " if respond_to?(:spm_dependency, true) && ENV['FREERASP_DISABLE_SPM'] != '1'", - " 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)', - ' end', + " 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 = From 245a6672b5ce137a1a5c259b1b89d8df42447c21 Mon Sep 17 00:00:00 2001 From: martinzigrai Date: Thu, 16 Jul 2026 16:17:58 +0200 Subject: [PATCH 13/15] fix: resolve Expo plugin eslint errors (no-shadow, avoid namespace imports) --- plugin/build/index.js | 43 ++++++++++--------------------------------- plugin/src/index.ts | 23 ++++++++++------------- 2 files changed, 20 insertions(+), 46 deletions(-) diff --git a/plugin/build/index.js b/plugin/build/index.js index ace016c..1d13d91 100644 --- a/plugin/build/index.js +++ b/plugin/build/index.js @@ -1,31 +1,8 @@ "use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; Object.defineProperty(exports, "__esModule", { value: true }); const config_plugins_1 = require("@expo/config-plugins"); -const fs = __importStar(require("fs")); -const path = __importStar(require("path")); +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'; @@ -105,9 +82,9 @@ 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, (config) => { - config.modResults['ios.useFrameworks'] = 'dynamic'; - return config; + return (0, config_plugins_1.withPodfileProperties)(config, (cfg) => { + cfg.modResults['ios.useFrameworks'] = 'dynamic'; + return cfg; }); }; /** @@ -117,9 +94,9 @@ const withFreeraspIosDynamicFrameworks = (config) => { const withFreeraspIosSpmEmbed = (config) => { return (0, config_plugins_1.withDangerousMod)(config, [ 'ios', - (config) => { - const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile'); - let contents = fs.readFileSync(podfilePath, 'utf-8'); + (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); @@ -139,10 +116,10 @@ const withFreeraspIosSpmEmbed = (config) => { const insertAt = anchorIndex + anchor.length; contents = contents.slice(0, insertAt) + snippet + contents.slice(insertAt); - fs.writeFileSync(podfilePath, contents); + (0, fs_1.writeFileSync)(podfilePath, contents); } } - return config; + return cfg; }, ]); }; diff --git a/plugin/src/index.ts b/plugin/src/index.ts index 0c9e788..8025fd9 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -8,8 +8,8 @@ import { type ConfigPlugin, } from '@expo/config-plugins'; import { type ExpoConfig } from '@expo/config-types'; -import * as fs from 'fs'; -import * as path from 'path'; +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; import { type PluginConfigType } from './pluginConfig'; const { createBuildGradlePropsConfigPlugin } = AndroidConfig.BuildProperties; @@ -121,9 +121,9 @@ 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, (config) => { - config.modResults['ios.useFrameworks'] = 'dynamic'; - return config; + return withPodfileProperties(config, (cfg) => { + cfg.modResults['ios.useFrameworks'] = 'dynamic'; + return cfg; }); }; @@ -134,12 +134,9 @@ const withFreeraspIosDynamicFrameworks: ConfigPlugin = (config) => { const withFreeraspIosSpmEmbed: ConfigPlugin = (config) => { return withDangerousMod(config, [ 'ios', - (config) => { - const podfilePath = path.join( - config.modRequest.platformProjectRoot, - 'Podfile' - ); - let contents = fs.readFileSync(podfilePath, 'utf-8'); + (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|'; @@ -162,10 +159,10 @@ const withFreeraspIosSpmEmbed: ConfigPlugin = (config) => { const insertAt = anchorIndex + anchor.length; contents = contents.slice(0, insertAt) + snippet + contents.slice(insertAt); - fs.writeFileSync(podfilePath, contents); + writeFileSync(podfilePath, contents); } } - return config; + return cfg; }, ]); }; From 98687517dfdc2922f73607cb1b520c3649f03472 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Mon, 20 Jul 2026 12:26:17 +0200 Subject: [PATCH 14/15] feat: keep package.swift in the project --- .gitignore | 1 + .../project.pbxproj | 51 +++++++++++++-- freerasp-react-native.podspec | 13 ++-- freerasp_spm.rb | 64 +++++++++++++------ ios/TalsecRuntimePackage/Package.swift | 19 ++++++ 5 files changed, 119 insertions(+), 29 deletions(-) create mode 100644 ios/TalsecRuntimePackage/Package.swift diff --git a/.gitignore b/.gitignore index 551c1d4..2c789b3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ xcuserdata *.xccheckout *.moved-aside DerivedData +.build/ *.hmap *.ipa *.xcuserstate diff --git a/example/ios/FreeraspRNExample.xcodeproj/project.pbxproj b/example/ios/FreeraspRNExample.xcodeproj/project.pbxproj index 25b485c..67ad0f4 100644 --- a/example/ios/FreeraspRNExample.xcodeproj/project.pbxproj +++ b/example/ios/FreeraspRNExample.xcodeproj/project.pbxproj @@ -7,11 +7,11 @@ objects = { /* Begin PBXBuildFile section */ - 0C80B921A6F3F58F76C31292 /* libPods-FreeraspRNExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-FreeraspRNExample.a */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; B1EB9AB7744AD93A8B6158B0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; + C73C359964238BC19E9222C4 /* Pods_FreeraspRNExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E5F6A1D4DB8919B5ABBB72F8 /* Pods_FreeraspRNExample.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -21,9 +21,9 @@ 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = FreeraspRNExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; 3B4392A12AC88292D35C810B /* Pods-FreeraspRNExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FreeraspRNExample.debug.xcconfig"; path = "Target Support Files/Pods-FreeraspRNExample/Pods-FreeraspRNExample.debug.xcconfig"; sourceTree = ""; }; 5709B34CF0A7D63546082F79 /* Pods-FreeraspRNExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FreeraspRNExample.release.xcconfig"; path = "Target Support Files/Pods-FreeraspRNExample/Pods-FreeraspRNExample.release.xcconfig"; sourceTree = ""; }; - 5DCACB8F33CDC322A6C60F78 /* libPods-FreeraspRNExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FreeraspRNExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = FreeraspRNExample/AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = FreeraspRNExample/LaunchScreen.storyboard; sourceTree = ""; }; + E5F6A1D4DB8919B5ABBB72F8 /* Pods_FreeraspRNExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FreeraspRNExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -32,7 +32,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0C80B921A6F3F58F76C31292 /* libPods-FreeraspRNExample.a in Frameworks */, + C73C359964238BC19E9222C4 /* Pods_FreeraspRNExample.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -55,7 +55,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 5DCACB8F33CDC322A6C60F78 /* libPods-FreeraspRNExample.a */, + E5F6A1D4DB8919B5ABBB72F8 /* Pods_FreeraspRNExample.framework */, ); name = Frameworks; sourceTree = ""; @@ -118,6 +118,9 @@ dependencies = ( ); name = FreeraspRNExample; + packageProductDependencies = ( + 48BC1057BF9DD44F5B4CFB92 /* TalsecRuntime */, + ); productName = FreeraspRNExample; productReference = 13B07F961A680F5B00A75B9A /* FreeraspRNExample.app */; productType = "com.apple.product-type.application"; @@ -144,6 +147,9 @@ Base, ); mainGroup = 83CBB9F61A601CBA00E9B192; + packageReferences = ( + B8E138E288186BEE6FC6ADBA /* XCLocalSwiftPackageReference "TalsecRuntimePackage" */, + ); productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -358,6 +364,17 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, @@ -431,6 +448,17 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, @@ -483,6 +511,21 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + B8E138E288186BEE6FC6ADBA /* XCLocalSwiftPackageReference "TalsecRuntimePackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "/Users/tpsota/Documents/Free-RASP-ReactNative/ios/TalsecRuntimePackage"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 48BC1057BF9DD44F5B4CFB92 /* TalsecRuntime */ = { + isa = XCSwiftPackageProductDependency; + package = B8E138E288186BEE6FC6ADBA /* XCLocalSwiftPackageReference "TalsecRuntimePackage" */; + productName = TalsecRuntime; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; } diff --git a/freerasp-react-native.podspec b/freerasp-react-native.podspec index 2ffc722..f2dbfc4 100644 --- a/freerasp-react-native.podspec +++ b/freerasp-react-native.podspec @@ -3,11 +3,10 @@ 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' +# TalsecRuntime: local SPM manifest by default (RN >= 0.75), vendored xcframework +# fallback. The manifest pins the remote binary URL and checksum. +# Opt out of SPM with FREERASP_DISABLE_SPM=1. +talsec_spm_path = File.expand_path('ios/TalsecRuntimePackage', __dir__) Pod::Spec.new do |s| s.name = "freerasp-react-native" @@ -36,8 +35,8 @@ Pod::Spec.new do |s| # 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 }, + url: talsec_spm_path, + requirement: {}, products: ['TalsecRuntime'] ) else diff --git a/freerasp_spm.rb b/freerasp_spm.rb index 7ec91db..315ef27 100644 --- a/freerasp_spm.rb +++ b/freerasp_spm.rb @@ -1,19 +1,30 @@ -# 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. +# freeRASP — links the local TalsecRuntime Swift package to the pod and embeds its +# product into the app target(s). React Native 0.75–0.83 treats local package paths +# as remote URLs, so this helper also replaces that invalid reference after +# react_native_post_install. It is idempotent and safe to call unconditionally. def freerasp_embed_talsec_spm!(installer, - url: 'https://github.com/talsec/Free-RASP-ReactNative-SPM', - requirement: { kind: 'exactVersion', version: '6.14.4' }, + package_path: File.expand_path('ios/TalsecRuntimePackage', __dir__), product: 'TalsecRuntime') - pkg_class = Xcodeproj::Project::Object::XCRemoteSwiftPackageReference + package_path = File.expand_path(package_path) + local_pkg_class = Xcodeproj::Project::Object::XCLocalSwiftPackageReference + remote_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' + if spm_active && !File.file?(File.join(package_path, 'Package.swift')) + raise Pod::Informative, "[freeRASP][SPM] Package.swift not found at #{package_path}" + end + + projects_and_targets = {} + pods_project = installer.pods_project + projects_and_targets[pods_project] = pods_project.targets.select do |target| + target.name == 'freerasp-react-native' + end + installer.aggregate_targets.each do |aggregate_target| project = aggregate_target.user_project next if project.nil? @@ -21,27 +32,44 @@ def freerasp_embed_talsec_spm!(installer, app_targets = aggregate_target.user_targets.select do |t| t.respond_to?(:product_type) && t.product_type == 'com.apple.product-type.application' end + projects_and_targets[project] ||= [] + projects_and_targets[project].concat(app_targets) + end - # Remove any previously-added reference first (avoids a double embed on delivery switch). - app_targets.each do |target| + new_object = lambda do |project, klass| + uuid = project.generate_uuid + uuid = project.generate_uuid while project.objects_by_uuid.key?(uuid) + object = klass.new(project, uuid) + object.initialize_defaults + object + end + + projects_and_targets.each do |project, targets| + targets.uniq! + + # Remove local and incorrectly-created remote references first. + 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 + next false unless r.class == ref_class && r.product_name == product + + package = r.package + (package.class == local_pkg_class && package.relative_path == package_path) || + (package.class == remote_pkg_class && package.repositoryURL == package_path) end end project.root_object.package_references.delete_if do |p| - p.class == pkg_class && p.repositoryURL == url + (p.class == local_pkg_class && p.relative_path == package_path) || + (p.class == remote_pkg_class && p.repositoryURL == package_path) end if spm_active - pkg = project.new(pkg_class) - pkg.repositoryURL = url - pkg.requirement = requirement + pkg = new_object.call(project, local_pkg_class) + pkg.relative_path = package_path 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) + targets.each do |target| + Pod::UI.puts "[freeRASP][SPM] Linking #{product} to target #{target.name}" + dep = new_object.call(project, ref_class) dep.package = pkg dep.product_name = product target.package_product_dependencies << dep diff --git a/ios/TalsecRuntimePackage/Package.swift b/ios/TalsecRuntimePackage/Package.swift new file mode 100644 index 0000000..4bab07c --- /dev/null +++ b/ios/TalsecRuntimePackage/Package.swift @@ -0,0 +1,19 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "TalsecRuntime", + platforms: [ + .iOS(.v13), + ], + products: [ + .library(name: "TalsecRuntime", targets: ["TalsecRuntime"]), + ], + targets: [ + .binaryTarget( + name: "TalsecRuntime", + url: "https://storage.googleapis.com/freerasp/ios/react-native/6.14.4/TalsecRuntime.zip", + checksum: "1ee2ba204688b4f059f9ab30b8f8c769f673f3412c7e46d52a2dc25b3561105b" + ), + ] +) From 2994e8583c7e8e4fa87e366c358e1c2fea25f614 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Mon, 20 Jul 2026 13:58:12 +0200 Subject: [PATCH 15/15] fix: harden iOS SPM integration Co-authored-by: Cursor --- .github/workflows/ci.yml | 4 + README.md | 43 +++++ .../project.pbxproj | 51 +---- example/ios/Podfile | 20 +- freerasp-react-native.podspec | 15 +- freerasp_spm.rb | 37 ++-- package.json | 1 + plugin/build/index.js | 54 +++--- plugin/build/iosSpm.d.ts | 7 + plugin/build/iosSpm.js | 137 +++++++++++++ plugin/build/iosSpmProperties.d.ts | 2 + plugin/build/iosSpmProperties.js | 29 +++ plugin/build/pluginConfig.d.ts | 17 ++ plugin/src/index.ts | 67 ++++--- plugin/src/iosSpm.ts | 181 ++++++++++++++++++ plugin/src/iosSpmProperties.ts | 31 +++ plugin/src/pluginConfig.ts | 19 ++ plugin/tests/iosSpm.test.js | 121 ++++++++++++ 18 files changed, 699 insertions(+), 137 deletions(-) create mode 100644 plugin/build/iosSpm.d.ts create mode 100644 plugin/build/iosSpm.js create mode 100644 plugin/build/iosSpmProperties.d.ts create mode 100644 plugin/build/iosSpmProperties.js create mode 100644 plugin/src/iosSpm.ts create mode 100644 plugin/src/iosSpmProperties.ts create mode 100644 plugin/tests/iosSpm.test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4836ead..62ca6ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,9 @@ jobs: run: | yarn expo:typecheck + - name: Test Expo plugin + run: yarn test:plugin + - name: Format check run: yarn prettier --check @@ -109,6 +112,7 @@ jobs: needs: build-library env: USE_FRAMEWORKS: dynamic + FREERASP_USE_SPM: "1" steps: - name: Checkout uses: actions/checkout@v6 diff --git a/README.md b/README.md index 0c4d383..873fb89 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,49 @@ For integrating freeRASP on the React Native platform, be sure to follow all the Be sure to bookmark it and stay informed! :books: :sparkles:. +### iOS Swift Package Manager delivery +Expo projects use Swift Package Manager for `TalsecRuntime` by default. To use +the vendored XCFramework instead, configure the plugin in `app.json`: + +```json +[ + "freerasp-react-native", + { + "ios": { + "useSpm": false + } + } +] +``` + +Swift Package Manager is opt-in for bare React Native projects and requires +React Native 0.75 or newer, iOS 13 or newer, and dynamically linked frameworks. +Set `FREERASP_USE_SPM` before dependencies are evaluated in the Podfile: + +```ruby +ENV['FREERASP_USE_SPM'] = '1' +use_frameworks! :linkage => :dynamic +``` + +Call the freeRASP helper after `react_native_post_install` in the same Podfile: + +```ruby +post_install do |installer| + react_native_post_install( + installer, + config[:reactNativePath] + ) + + 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) +end +``` + +Omit `FREERASP_USE_SPM`, set the Expo option to `false`, or set +`FREERASP_DISABLE_SPM=1` to use the vendored fallback. + # :rocket: What's New and Changelog Stay informed and make the most of freeRASP by checking out [What's New and Changelog](https://docs.talsec.app/freerasp/whats-new-and-changelog?utm_source=github)! Here, you’ll discover the latest features, enhancements, and bug fixes we’ve implemented to improve your experience across all platforms, including Android, iOS, Flutter, React Native, Capacitor, and Cordova. diff --git a/example/ios/FreeraspRNExample.xcodeproj/project.pbxproj b/example/ios/FreeraspRNExample.xcodeproj/project.pbxproj index 67ad0f4..25b485c 100644 --- a/example/ios/FreeraspRNExample.xcodeproj/project.pbxproj +++ b/example/ios/FreeraspRNExample.xcodeproj/project.pbxproj @@ -7,11 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 0C80B921A6F3F58F76C31292 /* libPods-FreeraspRNExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-FreeraspRNExample.a */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; B1EB9AB7744AD93A8B6158B0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; - C73C359964238BC19E9222C4 /* Pods_FreeraspRNExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E5F6A1D4DB8919B5ABBB72F8 /* Pods_FreeraspRNExample.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -21,9 +21,9 @@ 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = FreeraspRNExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; 3B4392A12AC88292D35C810B /* Pods-FreeraspRNExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FreeraspRNExample.debug.xcconfig"; path = "Target Support Files/Pods-FreeraspRNExample/Pods-FreeraspRNExample.debug.xcconfig"; sourceTree = ""; }; 5709B34CF0A7D63546082F79 /* Pods-FreeraspRNExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FreeraspRNExample.release.xcconfig"; path = "Target Support Files/Pods-FreeraspRNExample/Pods-FreeraspRNExample.release.xcconfig"; sourceTree = ""; }; + 5DCACB8F33CDC322A6C60F78 /* libPods-FreeraspRNExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-FreeraspRNExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = FreeraspRNExample/AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = FreeraspRNExample/LaunchScreen.storyboard; sourceTree = ""; }; - E5F6A1D4DB8919B5ABBB72F8 /* Pods_FreeraspRNExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FreeraspRNExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -32,7 +32,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C73C359964238BC19E9222C4 /* Pods_FreeraspRNExample.framework in Frameworks */, + 0C80B921A6F3F58F76C31292 /* libPods-FreeraspRNExample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -55,7 +55,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - E5F6A1D4DB8919B5ABBB72F8 /* Pods_FreeraspRNExample.framework */, + 5DCACB8F33CDC322A6C60F78 /* libPods-FreeraspRNExample.a */, ); name = Frameworks; sourceTree = ""; @@ -118,9 +118,6 @@ dependencies = ( ); name = FreeraspRNExample; - packageProductDependencies = ( - 48BC1057BF9DD44F5B4CFB92 /* TalsecRuntime */, - ); productName = FreeraspRNExample; productReference = 13B07F961A680F5B00A75B9A /* FreeraspRNExample.app */; productType = "com.apple.product-type.application"; @@ -147,9 +144,6 @@ Base, ); mainGroup = 83CBB9F61A601CBA00E9B192; - packageReferences = ( - B8E138E288186BEE6FC6ADBA /* XCLocalSwiftPackageReference "TalsecRuntimePackage" */, - ); productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -364,17 +358,6 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - HEADER_SEARCH_PATHS = ( - "$(inherited)", - "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", - "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", - "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", - "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", - "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", - ); IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, @@ -448,17 +431,6 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - HEADER_SEARCH_PATHS = ( - "$(inherited)", - "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", - "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", - "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", - "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", - "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", - ); IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, @@ -511,21 +483,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCLocalSwiftPackageReference section */ - B8E138E288186BEE6FC6ADBA /* XCLocalSwiftPackageReference "TalsecRuntimePackage" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = "/Users/tpsota/Documents/Free-RASP-ReactNative/ios/TalsecRuntimePackage"; - }; -/* End XCLocalSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 48BC1057BF9DD44F5B4CFB92 /* TalsecRuntime */ = { - isa = XCSwiftPackageProductDependency; - package = B8E138E288186BEE6FC6ADBA /* XCLocalSwiftPackageReference "TalsecRuntimePackage" */; - productName = TalsecRuntime; - }; -/* End XCSwiftPackageProductDependency section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; } diff --git a/example/ios/Podfile b/example/ios/Podfile index 8bce2a4..1c3fa63 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -8,12 +8,20 @@ require Pod::Executable.execute_command('node', ['-p', platform :ios, min_ios_version_supported prepare_react_native_project! -# 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 +# The example exercises the vendored fallback by default. Its SPM CI job sets +# FREERASP_USE_SPM=1; SPM requires dynamic frameworks. +use_freerasp_spm = ENV['FREERASP_USE_SPM'] == '1' && ENV['FREERASP_DISABLE_SPM'] != '1' +linkage = ENV['USE_FRAMEWORKS'] +if use_freerasp_spm + if linkage != nil && linkage != 'dynamic' + raise Pod::Informative, 'freeRASP SPM requires USE_FRAMEWORKS=dynamic' + end + linkage = 'dynamic' +end +if linkage != nil + Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green + use_frameworks! :linkage => linkage.to_sym +end target 'FreeraspRNExample' do config = use_native_modules! diff --git a/freerasp-react-native.podspec b/freerasp-react-native.podspec index f2dbfc4..efe11f6 100644 --- a/freerasp-react-native.podspec +++ b/freerasp-react-native.podspec @@ -3,9 +3,9 @@ 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: local SPM manifest by default (RN >= 0.75), vendored xcframework +# TalsecRuntime: opt-in local SPM manifest (RN >= 0.75), vendored xcframework # fallback. The manifest pins the remote binary URL and checksum. -# Opt out of SPM with FREERASP_DISABLE_SPM=1. +# Enable SPM with FREERASP_USE_SPM=1; FREERASP_DISABLE_SPM=1 always wins. talsec_spm_path = File.expand_path('ios/TalsecRuntimePackage', __dir__) Pod::Spec.new do |s| @@ -19,9 +19,12 @@ Pod::Spec.new do |s| s.platforms = { :ios => "11.0" } s.source = { :git => "https://github.com/talsec/freerasp-react-native.git", :tag => "#{s.version}" } - # 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' + spm_requested = ENV['FREERASP_USE_SPM'] == '1' && ENV['FREERASP_DISABLE_SPM'] != '1' + use_spm = spm_requested && respond_to?(:spm_dependency, true) + + if spm_requested && !respond_to?(:spm_dependency, true) + Pod::UI.warn '[freeRASP][SPM] React Native does not provide spm_dependency; using the vendored TalsecRuntime framework.' + end source_globs = [ 'ios/models/*.{h,m,mm,swift}', @@ -40,7 +43,7 @@ Pod::Spec.new do |s| products: ['TalsecRuntime'] ) else - # Vendored xcframework fallback (RN < 0.75 or FREERASP_DISABLE_SPM=1). + # Vendored xcframework fallback (default, 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' diff --git a/freerasp_spm.rb b/freerasp_spm.rb index 315ef27..ad48422 100644 --- a/freerasp_spm.rb +++ b/freerasp_spm.rb @@ -12,8 +12,10 @@ def freerasp_embed_talsec_spm!(installer, remote_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' + # Mirror the podspec: SPM is explicit and requires RN's spm_dependency helper. + spm_active = ENV['FREERASP_USE_SPM'] == '1' && + ENV['FREERASP_DISABLE_SPM'] != '1' && + respond_to?(:spm_dependency, true) if spm_active && !File.file?(File.join(package_path, 'Package.swift')) raise Pod::Informative, "[freeRASP][SPM] Package.swift not found at #{package_path}" @@ -47,19 +49,30 @@ def freerasp_embed_talsec_spm!(installer, projects_and_targets.each do |project, targets| targets.uniq! - # Remove local and incorrectly-created remote references first. + # Remove references owned by this integration regardless of their previous + # absolute path. This also cleans references committed from another checkout. + owned_packages = [] targets.each do |target| - target.package_product_dependencies.delete_if do |r| - next false unless r.class == ref_class && r.product_name == product - - package = r.package - (package.class == local_pkg_class && package.relative_path == package_path) || - (package.class == remote_pkg_class && package.repositoryURL == package_path) + target.package_product_dependencies.select do |reference| + reference.class == ref_class && reference.product_name == product + end.each do |reference| + owned_packages << reference.package unless reference.package.nil? + reference.remove_from_project end end - project.root_object.package_references.delete_if do |p| - (p.class == local_pkg_class && p.relative_path == package_path) || - (p.class == remote_pkg_class && p.repositoryURL == package_path) + project.root_object.package_references.each do |package| + reference_path = + if package.class == local_pkg_class + package.relative_path + elsif package.class == remote_pkg_class + package.repositoryURL + end + next if reference_path.nil? + + owned_packages << package if File.basename(reference_path) == 'TalsecRuntimePackage' + end + owned_packages.uniq.each do |package| + package.remove_from_project end if spm_active diff --git a/package.json b/package.json index ba22a47..57db19e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "prettier": "prettier \"**/*.{ts,js}\"", "prepack": "bob build && yarn build:plugin", "build": "bob build", + "test:plugin": "yarn build:plugin && node plugin/tests/iosSpm.test.js", "example": "yarn --cwd example", "bootstrap": "yarn example && yarn install && yarn example pods", "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build", diff --git a/plugin/build/index.js b/plugin/build/index.js index 1d13d91..2f38384 100644 --- a/plugin/build/index.js +++ b/plugin/build/index.js @@ -1,8 +1,13 @@ "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; 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 iosSpm_1 = __importDefault(require("./iosSpm")); +const iosSpmProperties_1 = __importDefault(require("./iosSpmProperties")); 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'; @@ -77,13 +82,13 @@ const withAndroidR8Version = (expoConfig, props) => { // 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`. + * Configure dynamic frameworks while SPM is active and remove only values + * previously managed by this plugin when switching back to the vendored path. */ -const withFreeraspIosDynamicFrameworks = (config) => { +const withFreeraspIosFrameworks = (config, spmEnabled) => { return (0, config_plugins_1.withPodfileProperties)(config, (cfg) => { - cfg.modResults['ios.useFrameworks'] = 'dynamic'; + (0, iosSpmProperties_1.default)(cfg.modResults, spmEnabled); return cfg; }); }; @@ -91,48 +96,35 @@ const withFreeraspIosDynamicFrameworks = (config) => { * 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) => { +const withFreeraspIosPodfile = (config, props) => { 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); - } + const contents = (0, fs_1.readFileSync)(podfilePath, 'utf-8'); + const result = (0, iosSpm_1.default)(contents, props?.spmEnabled ?? true); + if (result.missingAnchors.length > 0) { + config_plugins_1.WarningAggregator.addWarningIOS('freerasp-react-native', 'Could not configure TalsecRuntime delivery because the Podfile is missing: ' + + result.missingAnchors.join(', ')); + } + else if (result.changed) { + (0, fs_1.writeFileSync)(podfilePath, result.contents); } return cfg; }, ]); }; -const withRnTalsecIos = (config) => { - config = withFreeraspIosDynamicFrameworks(config); - config = withFreeraspIosSpmEmbed(config); +const withRnTalsecIos = (config, props) => { + const spmEnabled = props?.ios?.useSpm !== false && process.env.FREERASP_DISABLE_SPM !== '1'; + config = withFreeraspIosFrameworks(config, spmEnabled); + config = withFreeraspIosPodfile(config, { spmEnabled }); return config; }; const withRnTalsecApp = (config, props) => { config = withBuildscriptDependency(config); config = withAndroidMinSdkVersion(config, props); config = withAndroidR8Version(config, props); - config = withRnTalsecIos(config); + config = withRnTalsecIos(config, props); return config; }; let pkg = { diff --git a/plugin/build/iosSpm.d.ts b/plugin/build/iosSpm.d.ts new file mode 100644 index 0000000..9fd6b22 --- /dev/null +++ b/plugin/build/iosSpm.d.ts @@ -0,0 +1,7 @@ +export interface PodfileMutationResult { + contents: string; + changed: boolean; + missingAnchors: string[]; +} +declare const mutatePodfileForFreeraspSpm: (contents: string, enabled?: boolean) => PodfileMutationResult; +export default mutatePodfileForFreeraspSpm; diff --git a/plugin/build/iosSpm.js b/plugin/build/iosSpm.js new file mode 100644 index 0000000..0e861e2 --- /dev/null +++ b/plugin/build/iosSpm.js @@ -0,0 +1,137 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const FREERASP_SPM_ACTIVATION_TAG = '# @generated freerasp-react-native (SPM activation)'; +const FREERASP_SPM_DISABLED_TAG = '# @generated freerasp-react-native (SPM disabled)'; +const FREERASP_SPM_EMBED_TAG = '# @generated freerasp-react-native (SPM embed)'; +const PREPARE_REACT_NATIVE_ANCHOR = 'prepare_react_native_project!'; +const REACT_NATIVE_POST_INSTALL_ANCHOR = 'react_native_post_install('; +const indentationAt = (contents, index) => { + const lineStart = contents.lastIndexOf('\n', index - 1) + 1; + return contents.slice(lineStart, index).match(/^\s*/)?.[0] ?? ''; +}; +const removeManagedBlock = (contents, tag, bodyLineCount) => { + const newline = contents.includes('\r\n') ? '\r\n' : '\n'; + const lines = contents.split(/\r?\n/); + const tagIndex = lines.findIndex((line) => line.trim() === tag); + if (tagIndex === -1) { + return contents; + } + let startIndex = tagIndex; + while (startIndex > 0 && lines[startIndex - 1]?.trim() === '') { + startIndex -= 1; + } + lines.splice(startIndex, tagIndex - startIndex + bodyLineCount + 1); + return lines.join(newline); +}; +const findClosingParenthesis = (contents, openingIndex) => { + let depth = 0; + let quote; + let escaped = false; + let inComment = false; + for (let index = openingIndex; index < contents.length; index += 1) { + const character = contents[index]; + if (inComment) { + if (character === '\n') { + inComment = false; + } + continue; + } + if (quote) { + if (escaped) { + escaped = false; + } + else if (character === '\\') { + escaped = true; + } + else if (character === quote) { + quote = undefined; + } + continue; + } + if (character === '#') { + inComment = true; + } + else if (character === "'" || character === '"') { + quote = character; + } + else if (character === '(') { + depth += 1; + } + else if (character === ')') { + depth -= 1; + if (depth === 0) { + return index; + } + } + } + return -1; +}; +const mutatePodfileForFreeraspSpm = (contents, enabled = true) => { + let updatedContents = removeManagedBlock(contents, FREERASP_SPM_ACTIVATION_TAG, 1); + updatedContents = removeManagedBlock(updatedContents, FREERASP_SPM_DISABLED_TAG, 1); + updatedContents = removeManagedBlock(updatedContents, FREERASP_SPM_EMBED_TAG, 4); + const missingAnchors = []; + const prepareIndex = updatedContents.indexOf(PREPARE_REACT_NATIVE_ANCHOR); + const postInstallIndex = enabled + ? updatedContents.indexOf(REACT_NATIVE_POST_INSTALL_ANCHOR) + : -1; + const postInstallOpeningIndex = postInstallIndex === -1 + ? -1 + : postInstallIndex + REACT_NATIVE_POST_INSTALL_ANCHOR.length - 1; + const postInstallClosingIndex = postInstallOpeningIndex === -1 + ? -1 + : findClosingParenthesis(updatedContents, postInstallOpeningIndex); + if (prepareIndex === -1) { + missingAnchors.push(PREPARE_REACT_NATIVE_ANCHOR); + } + if (enabled && postInstallClosingIndex === -1) { + missingAnchors.push(REACT_NATIVE_POST_INSTALL_ANCHOR); + } + if (missingAnchors.length > 0) { + return { contents, changed: false, missingAnchors }; + } + const newline = updatedContents.includes('\r\n') ? '\r\n' : '\n'; + if (enabled) { + // Insert the later snippet first so the activation insertion cannot + // invalidate the post-install index calculated above. + const indent = indentationAt(updatedContents, postInstallIndex); + const snippet = [ + '', + '', + `${indent}${FREERASP_SPM_EMBED_TAG}`, + `${indent}require Pod::Executable.execute_command('node', ['-p',`, + `${indent} 'require.resolve("freerasp-react-native/freerasp_spm.rb", {paths: [process.argv[1]]})',`, + `${indent} __dir__]).strip`, + `${indent}freerasp_embed_talsec_spm!(installer)`, + ].join(newline); + updatedContents = + updatedContents.slice(0, postInstallClosingIndex + 1) + + snippet + + updatedContents.slice(postInstallClosingIndex + 1); + } + const insertAt = prepareIndex + PREPARE_REACT_NATIVE_ANCHOR.length; + const indent = indentationAt(updatedContents, prepareIndex); + const configurationSnippet = enabled + ? [ + '', + '', + `${indent}${FREERASP_SPM_ACTIVATION_TAG}`, + `${indent}ENV['FREERASP_USE_SPM'] = '1' unless ENV['FREERASP_DISABLE_SPM'] == '1'`, + ] + : [ + '', + '', + `${indent}${FREERASP_SPM_DISABLED_TAG}`, + `${indent}ENV['FREERASP_DISABLE_SPM'] = '1'`, + ]; + updatedContents = + updatedContents.slice(0, insertAt) + + configurationSnippet.join(newline) + + updatedContents.slice(insertAt); + return { + contents: updatedContents, + changed: updatedContents !== contents, + missingAnchors: [], + }; +}; +exports.default = mutatePodfileForFreeraspSpm; diff --git a/plugin/build/iosSpmProperties.d.ts b/plugin/build/iosSpmProperties.d.ts new file mode 100644 index 0000000..3a68c33 --- /dev/null +++ b/plugin/build/iosSpmProperties.d.ts @@ -0,0 +1,2 @@ +declare const configureIosSpmProperties: (properties: Record, spmEnabled: boolean) => Record; +export default configureIosSpmProperties; diff --git a/plugin/build/iosSpmProperties.js b/plugin/build/iosSpmProperties.js new file mode 100644 index 0000000..17e4590 --- /dev/null +++ b/plugin/build/iosSpmProperties.js @@ -0,0 +1,29 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const FRAMEWORKS_PROPERTY = 'ios.useFrameworks'; +const MANAGED_PROPERTY = 'freerasp.iosUseFrameworks'; +const PREVIOUS_VALUE_PROPERTY = 'freerasp.previousIosUseFrameworks'; +const UNSET_VALUE = '__freerasp_unset__'; +const configureIosSpmProperties = (properties, spmEnabled) => { + if (spmEnabled) { + if (properties[MANAGED_PROPERTY] !== 'true') { + properties[PREVIOUS_VALUE_PROPERTY] = + properties[FRAMEWORKS_PROPERTY] ?? UNSET_VALUE; + } + properties[FRAMEWORKS_PROPERTY] = 'dynamic'; + properties[MANAGED_PROPERTY] = 'true'; + } + else if (properties[MANAGED_PROPERTY] === 'true') { + const previousValue = properties[PREVIOUS_VALUE_PROPERTY]; + if (previousValue && previousValue !== UNSET_VALUE) { + properties[FRAMEWORKS_PROPERTY] = previousValue; + } + else { + delete properties[FRAMEWORKS_PROPERTY]; + } + delete properties[MANAGED_PROPERTY]; + delete properties[PREVIOUS_VALUE_PROPERTY]; + } + return properties; +}; +exports.default = configureIosSpmProperties; diff --git a/plugin/build/pluginConfig.d.ts b/plugin/build/pluginConfig.d.ts index 3121ba3..1dc3e0b 100644 --- a/plugin/build/pluginConfig.d.ts +++ b/plugin/build/pluginConfig.d.ts @@ -2,12 +2,29 @@ * Interface representing base build properties configuration. */ export interface PluginConfigType { + /** + * Interface representing available configuration for the iOS integration. + * @platform ios + */ + ios?: PluginConfigTypeIos; /** * Interface representing available configuration for Android native build properties. * @platform android */ android?: PluginConfigTypeAndroid; } +/** + * Interface representing available configuration for the iOS integration. + * @platform ios + */ +export interface PluginConfigTypeIos { + /** + * Deliver TalsecRuntime through Swift Package Manager. + * + * Defaults to `true`. Set this to `false` to use the vendored XCFramework. + */ + useSpm?: boolean; +} /** * Interface representing available configuration for Android native build properties. * @platform android diff --git a/plugin/src/index.ts b/plugin/src/index.ts index 8025fd9..ca503a1 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -10,6 +10,8 @@ import { import { type ExpoConfig } from '@expo/config-types'; import { readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; +import mutatePodfileForFreeraspSpm from './iosSpm'; +import configureIosSpmProperties from './iosSpmProperties'; import { type PluginConfigType } from './pluginConfig'; const { createBuildGradlePropsConfigPlugin } = AndroidConfig.BuildProperties; @@ -115,14 +117,16 @@ const withAndroidR8Version: ConfigPlugin = ( // 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`. + * Configure dynamic frameworks while SPM is active and remove only values + * previously managed by this plugin when switching back to the vendored path. */ -const withFreeraspIosDynamicFrameworks: ConfigPlugin = (config) => { +const withFreeraspIosFrameworks = ( + config: ExpoConfig, + spmEnabled: boolean +): ExpoConfig => { return withPodfileProperties(config, (cfg) => { - cfg.modResults['ios.useFrameworks'] = 'dynamic'; + configureIosSpmProperties(cfg.modResults, spmEnabled); return cfg; }); }; @@ -131,45 +135,38 @@ const withFreeraspIosDynamicFrameworks: ConfigPlugin = (config) => { * 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) => { +const withFreeraspIosPodfile: ConfigPlugin<{ + spmEnabled: boolean; +}> = (config, props) => { 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); - } + const contents = readFileSync(podfilePath, 'utf-8'); + const result = mutatePodfileForFreeraspSpm( + contents, + props?.spmEnabled ?? true + ); + + if (result.missingAnchors.length > 0) { + WarningAggregator.addWarningIOS( + 'freerasp-react-native', + 'Could not configure TalsecRuntime delivery because the Podfile is missing: ' + + result.missingAnchors.join(', ') + ); + } else if (result.changed) { + writeFileSync(podfilePath, result.contents); } return cfg; }, ]); }; -const withRnTalsecIos: ConfigPlugin = (config) => { - config = withFreeraspIosDynamicFrameworks(config); - config = withFreeraspIosSpmEmbed(config); +const withRnTalsecIos: ConfigPlugin = (config, props) => { + const spmEnabled = + props?.ios?.useSpm !== false && process.env.FREERASP_DISABLE_SPM !== '1'; + config = withFreeraspIosFrameworks(config, spmEnabled); + config = withFreeraspIosPodfile(config, { spmEnabled }); return config; }; @@ -177,7 +174,7 @@ const withRnTalsecApp: ConfigPlugin = (config, props) => { config = withBuildscriptDependency(config); config = withAndroidMinSdkVersion(config, props); config = withAndroidR8Version(config, props); - config = withRnTalsecIos(config); + config = withRnTalsecIos(config, props); return config; }; diff --git a/plugin/src/iosSpm.ts b/plugin/src/iosSpm.ts new file mode 100644 index 0000000..bc1000c --- /dev/null +++ b/plugin/src/iosSpm.ts @@ -0,0 +1,181 @@ +const FREERASP_SPM_ACTIVATION_TAG = + '# @generated freerasp-react-native (SPM activation)'; +const FREERASP_SPM_DISABLED_TAG = + '# @generated freerasp-react-native (SPM disabled)'; +const FREERASP_SPM_EMBED_TAG = '# @generated freerasp-react-native (SPM embed)'; + +const PREPARE_REACT_NATIVE_ANCHOR = 'prepare_react_native_project!'; +const REACT_NATIVE_POST_INSTALL_ANCHOR = 'react_native_post_install('; + +export interface PodfileMutationResult { + contents: string; + changed: boolean; + missingAnchors: string[]; +} + +const indentationAt = (contents: string, index: number): string => { + const lineStart = contents.lastIndexOf('\n', index - 1) + 1; + return contents.slice(lineStart, index).match(/^\s*/)?.[0] ?? ''; +}; + +const removeManagedBlock = ( + contents: string, + tag: string, + bodyLineCount: number +): string => { + const newline = contents.includes('\r\n') ? '\r\n' : '\n'; + const lines = contents.split(/\r?\n/); + const tagIndex = lines.findIndex((line) => line.trim() === tag); + + if (tagIndex === -1) { + return contents; + } + + let startIndex = tagIndex; + while (startIndex > 0 && lines[startIndex - 1]?.trim() === '') { + startIndex -= 1; + } + lines.splice(startIndex, tagIndex - startIndex + bodyLineCount + 1); + return lines.join(newline); +}; + +const findClosingParenthesis = ( + contents: string, + openingIndex: number +): number => { + let depth = 0; + let quote: "'" | '"' | undefined; + let escaped = false; + let inComment = false; + + for (let index = openingIndex; index < contents.length; index += 1) { + const character = contents[index]; + + if (inComment) { + if (character === '\n') { + inComment = false; + } + continue; + } + + if (quote) { + if (escaped) { + escaped = false; + } else if (character === '\\') { + escaped = true; + } else if (character === quote) { + quote = undefined; + } + continue; + } + + if (character === '#') { + inComment = true; + } else if (character === "'" || character === '"') { + quote = character; + } else if (character === '(') { + depth += 1; + } else if (character === ')') { + depth -= 1; + if (depth === 0) { + return index; + } + } + } + + return -1; +}; + +const mutatePodfileForFreeraspSpm = ( + contents: string, + enabled = true +): PodfileMutationResult => { + let updatedContents = removeManagedBlock( + contents, + FREERASP_SPM_ACTIVATION_TAG, + 1 + ); + updatedContents = removeManagedBlock( + updatedContents, + FREERASP_SPM_DISABLED_TAG, + 1 + ); + updatedContents = removeManagedBlock( + updatedContents, + FREERASP_SPM_EMBED_TAG, + 4 + ); + + const missingAnchors: string[] = []; + const prepareIndex = updatedContents.indexOf(PREPARE_REACT_NATIVE_ANCHOR); + const postInstallIndex = enabled + ? updatedContents.indexOf(REACT_NATIVE_POST_INSTALL_ANCHOR) + : -1; + const postInstallOpeningIndex = + postInstallIndex === -1 + ? -1 + : postInstallIndex + REACT_NATIVE_POST_INSTALL_ANCHOR.length - 1; + const postInstallClosingIndex = + postInstallOpeningIndex === -1 + ? -1 + : findClosingParenthesis(updatedContents, postInstallOpeningIndex); + + if (prepareIndex === -1) { + missingAnchors.push(PREPARE_REACT_NATIVE_ANCHOR); + } + if (enabled && postInstallClosingIndex === -1) { + missingAnchors.push(REACT_NATIVE_POST_INSTALL_ANCHOR); + } + if (missingAnchors.length > 0) { + return { contents, changed: false, missingAnchors }; + } + + const newline = updatedContents.includes('\r\n') ? '\r\n' : '\n'; + + if (enabled) { + // Insert the later snippet first so the activation insertion cannot + // invalidate the post-install index calculated above. + const indent = indentationAt(updatedContents, postInstallIndex); + const snippet = [ + '', + '', + `${indent}${FREERASP_SPM_EMBED_TAG}`, + `${indent}require Pod::Executable.execute_command('node', ['-p',`, + `${indent} 'require.resolve("freerasp-react-native/freerasp_spm.rb", {paths: [process.argv[1]]})',`, + `${indent} __dir__]).strip`, + `${indent}freerasp_embed_talsec_spm!(installer)`, + ].join(newline); + updatedContents = + updatedContents.slice(0, postInstallClosingIndex + 1) + + snippet + + updatedContents.slice(postInstallClosingIndex + 1); + } + + const insertAt = prepareIndex + PREPARE_REACT_NATIVE_ANCHOR.length; + const indent = indentationAt(updatedContents, prepareIndex); + const configurationSnippet = enabled + ? [ + '', + '', + `${indent}${FREERASP_SPM_ACTIVATION_TAG}`, + `${indent}ENV['FREERASP_USE_SPM'] = '1' unless ENV['FREERASP_DISABLE_SPM'] == '1'`, + ] + : [ + '', + '', + `${indent}${FREERASP_SPM_DISABLED_TAG}`, + `${indent}ENV['FREERASP_DISABLE_SPM'] = '1'`, + ]; + updatedContents = + updatedContents.slice(0, insertAt) + + configurationSnippet.join(newline) + + updatedContents.slice(insertAt); + + return { + contents: updatedContents, + changed: updatedContents !== contents, + missingAnchors: [], + }; +}; + +export default mutatePodfileForFreeraspSpm; diff --git a/plugin/src/iosSpmProperties.ts b/plugin/src/iosSpmProperties.ts new file mode 100644 index 0000000..6aa6082 --- /dev/null +++ b/plugin/src/iosSpmProperties.ts @@ -0,0 +1,31 @@ +const FRAMEWORKS_PROPERTY = 'ios.useFrameworks'; +const MANAGED_PROPERTY = 'freerasp.iosUseFrameworks'; +const PREVIOUS_VALUE_PROPERTY = 'freerasp.previousIosUseFrameworks'; +const UNSET_VALUE = '__freerasp_unset__'; + +const configureIosSpmProperties = ( + properties: Record, + spmEnabled: boolean +): Record => { + if (spmEnabled) { + if (properties[MANAGED_PROPERTY] !== 'true') { + properties[PREVIOUS_VALUE_PROPERTY] = + properties[FRAMEWORKS_PROPERTY] ?? UNSET_VALUE; + } + properties[FRAMEWORKS_PROPERTY] = 'dynamic'; + properties[MANAGED_PROPERTY] = 'true'; + } else if (properties[MANAGED_PROPERTY] === 'true') { + const previousValue = properties[PREVIOUS_VALUE_PROPERTY]; + if (previousValue && previousValue !== UNSET_VALUE) { + properties[FRAMEWORKS_PROPERTY] = previousValue; + } else { + delete properties[FRAMEWORKS_PROPERTY]; + } + delete properties[MANAGED_PROPERTY]; + delete properties[PREVIOUS_VALUE_PROPERTY]; + } + + return properties; +}; + +export default configureIosSpmProperties; diff --git a/plugin/src/pluginConfig.ts b/plugin/src/pluginConfig.ts index 075ef0f..b9e0a31 100644 --- a/plugin/src/pluginConfig.ts +++ b/plugin/src/pluginConfig.ts @@ -2,6 +2,12 @@ * Interface representing base build properties configuration. */ export interface PluginConfigType { + /** + * Interface representing available configuration for the iOS integration. + * @platform ios + */ + ios?: PluginConfigTypeIos; + /** * Interface representing available configuration for Android native build properties. * @platform android @@ -9,6 +15,19 @@ export interface PluginConfigType { android?: PluginConfigTypeAndroid; } +/** + * Interface representing available configuration for the iOS integration. + * @platform ios + */ +export interface PluginConfigTypeIos { + /** + * Deliver TalsecRuntime through Swift Package Manager. + * + * Defaults to `true`. Set this to `false` to use the vendored XCFramework. + */ + useSpm?: boolean; +} + /** * Interface representing available configuration for Android native build properties. * @platform android diff --git a/plugin/tests/iosSpm.test.js b/plugin/tests/iosSpm.test.js new file mode 100644 index 0000000..7fc00e0 --- /dev/null +++ b/plugin/tests/iosSpm.test.js @@ -0,0 +1,121 @@ +/* eslint-env node */ + +const assert = require('assert').strict; +const mutatePodfileForFreeraspSpm = require('../build/iosSpm').default; +const configureIosSpmProperties = require('../build/iosSpmProperties').default; + +const FREERASP_SPM_ACTIVATION_TAG = + '# @generated freerasp-react-native (SPM activation)'; +const FREERASP_SPM_DISABLED_TAG = + '# @generated freerasp-react-native (SPM disabled)'; +const FREERASP_SPM_EMBED_TAG = '# @generated freerasp-react-native (SPM embed)'; + +const podfile = `prepare_react_native_project! + +target 'Example' do + post_install do |installer| + react_native_post_install( + installer, + config[:reactNativePath], + :mac_catalyst_enabled => nested_option(enabled: false), + :quoted_value => "parentheses do not count: ())" + ) + end +end +`; + +const mutation = mutatePodfileForFreeraspSpm(podfile); +assert.equal(mutation.changed, true); +assert.deepEqual(mutation.missingAnchors, []); + +const activationIndex = mutation.contents.indexOf(FREERASP_SPM_ACTIVATION_TAG); +const targetIndex = mutation.contents.indexOf("target 'Example'"); +assert( + activationIndex > mutation.contents.indexOf('prepare_react_native_project!') +); +assert(activationIndex < targetIndex); + +const postInstallCloseIndex = mutation.contents.indexOf('\n )'); +const embedIndex = mutation.contents.indexOf(FREERASP_SPM_EMBED_TAG); +const postInstallEndIndex = mutation.contents.indexOf('\n end', embedIndex); +assert(embedIndex > postInstallCloseIndex); +assert(embedIndex < postInstallEndIndex); + +const secondMutation = mutatePodfileForFreeraspSpm(mutation.contents); +assert.equal(secondMutation.changed, false); +assert.equal(secondMutation.contents, mutation.contents); + +const disabledMutation = mutatePodfileForFreeraspSpm(mutation.contents, false); +assert.equal(disabledMutation.changed, true); +assert(disabledMutation.contents.includes(FREERASP_SPM_DISABLED_TAG)); +assert(disabledMutation.contents.includes("ENV['FREERASP_DISABLE_SPM'] = '1'")); +assert.equal( + disabledMutation.contents.includes(FREERASP_SPM_ACTIVATION_TAG), + false +); +assert.equal(disabledMutation.contents.includes(FREERASP_SPM_EMBED_TAG), false); + +const secondDisabledMutation = mutatePodfileForFreeraspSpm( + disabledMutation.contents, + false +); +assert.equal(secondDisabledMutation.changed, false); +assert.equal(secondDisabledMutation.contents, disabledMutation.contents); + +const reenabledMutation = mutatePodfileForFreeraspSpm( + disabledMutation.contents, + true +); +assert.equal(reenabledMutation.changed, true); +assert(reenabledMutation.contents.includes(FREERASP_SPM_ACTIVATION_TAG)); +assert(reenabledMutation.contents.includes(FREERASP_SPM_EMBED_TAG)); +assert.equal( + reenabledMutation.contents.includes(FREERASP_SPM_DISABLED_TAG), + false +); + +const missingAnchors = mutatePodfileForFreeraspSpm( + 'target "Example" do\nend\n' +); +assert.equal(missingAnchors.changed, false); +assert.equal(missingAnchors.contents, 'target "Example" do\nend\n'); +assert.deepEqual(missingAnchors.missingAnchors, [ + 'prepare_react_native_project!', + 'react_native_post_install(', +]); + +const windowsMutation = mutatePodfileForFreeraspSpm( + podfile.replace(/\n/g, '\r\n') +); +assert.equal(windowsMutation.changed, true); +assert.equal( + windowsMutation.contents.replace(/\r\n/g, '').includes('\n'), + false +); + +const propertiesWithoutFrameworks = {}; +configureIosSpmProperties(propertiesWithoutFrameworks, true); +assert.equal(propertiesWithoutFrameworks['ios.useFrameworks'], 'dynamic'); +configureIosSpmProperties(propertiesWithoutFrameworks, false); +assert.deepEqual(propertiesWithoutFrameworks, {}); + +const propertiesWithStaticFrameworks = { + 'ios.useFrameworks': 'static', +}; +configureIosSpmProperties(propertiesWithStaticFrameworks, true); +configureIosSpmProperties(propertiesWithStaticFrameworks, true); +assert.equal(propertiesWithStaticFrameworks['ios.useFrameworks'], 'dynamic'); +configureIosSpmProperties(propertiesWithStaticFrameworks, false); +assert.deepEqual(propertiesWithStaticFrameworks, { + 'ios.useFrameworks': 'static', +}); + +const unownedProperties = { + 'ios.useFrameworks': 'dynamic', +}; +configureIosSpmProperties(unownedProperties, false); +assert.deepEqual(unownedProperties, { + 'ios.useFrameworks': 'dynamic', +}); + +console.log('Expo iOS SPM tests passed.');