diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index b908d0f0061a..7269264395ea 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -203,6 +203,40 @@ jobs: fail-on-error: true secrets: inherit + test_ios_spm_rntester: + needs: + [ + prebuild_apple_dependencies, + prebuild_react_native_core, + check_code_changes, + ] + if: ${{ needs.prebuild_react_native_core.result == 'success' && needs.check_code_changes.outputs.should_test_ios == 'true' }} + uses: ./.github/workflows/test-ios-spm-rntester.yml + secrets: inherit + + test_ios_spm_helloworld: + needs: + [ + prebuild_apple_dependencies, + prebuild_react_native_core, + check_code_changes, + ] + if: ${{ needs.prebuild_react_native_core.result == 'success' && needs.check_code_changes.outputs.should_test_ios == 'true' }} + uses: ./.github/workflows/test-ios-spm-helloworld.yml + secrets: inherit + + test_ios_spm_newapp: + needs: + [ + build_npm_package, + prebuild_apple_dependencies, + prebuild_react_native_core, + check_code_changes, + ] + if: ${{ needs.prebuild_react_native_core.result == 'success' && needs.build_npm_package.result == 'success' && needs.check_code_changes.outputs.should_test_ios == 'true' }} + uses: ./.github/workflows/test-ios-spm-newapp.yml + secrets: inherit + test_e2e_android_templateapp: needs: [build_npm_package, build_android] if: ${{ always() && needs.build_android.result == 'success' && needs.build_npm_package.result == 'success' }} diff --git a/.github/workflows/test-ios-spm-helloworld.yml b/.github/workflows/test-ios-spm-helloworld.yml new file mode 100644 index 000000000000..6dcad4a07fdb --- /dev/null +++ b/.github/workflows/test-ios-spm-helloworld.yml @@ -0,0 +1,151 @@ +name: Test iOS SwiftPM - Hello World + +permissions: + contents: read + +on: + workflow_call: + +jobs: + test: + runs-on: macos-15-large + # TODO: remove once this new lane has proven green on main. Keeps a + # brand-new SwiftPM build from hard-blocking unrelated iOS PRs. + continue-on-error: true + strategy: + fail-fast: false + matrix: + flavor: [Debug, Release] + env: + APP_IOS_DIR: private/helloworld/ios + XCODE_PROJECT: HelloWorld.xcodeproj + XCODE_SCHEME: HelloWorld + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Setup xcode + uses: ./.github/actions/setup-xcode + - name: Setup node.js + uses: ./.github/actions/setup-node + - name: Run yarn install + uses: ./.github/actions/yarn-install + - name: Set Hermes prebuilt version + shell: bash + run: node ./scripts/releases/use-hermes-prebuilt.js + - name: Run yarn install again, with the correct hermes version + uses: ./.github/actions/yarn-install + - name: Ensure CocoaPods (`spm add --deintegrate` shells out to `pod`) + shell: bash + run: pod --version || sudo gem install cocoapods --no-document + # Both flavors are needed by every matrix cell, not just the one it builds: + # `spm add` stages both flavor framework trees and lets a per-configuration + # build setting pick one at build time, so it validates the debug/ and + # release/ artifact slots together (and `--download skip` refuses an + # incomplete slot). Please don't "optimise" this down to one flavor. + - name: Download ReactCore (Debug) + uses: actions/download-artifact@v7 + with: + name: ReactCoreDebug.xcframework.tar.gz + path: /tmp/rc-debug + - name: Download ReactCore (Release) + uses: actions/download-artifact@v7 + with: + name: ReactCoreRelease.xcframework.tar.gz + path: /tmp/rc-release + - name: Download ReactNativeDependencies (Debug) + uses: actions/download-artifact@v7 + with: + name: ReactNativeDependenciesDebug.xcframework.tar.gz + path: /tmp/deps-debug + - name: Download ReactNativeDependencies (Release) + uses: actions/download-artifact@v7 + with: + name: ReactNativeDependenciesRelease.xcframework.tar.gz + path: /tmp/deps-release + # `spm download` primes both flavor slots per call and skips any slot that + # already validates, so prime in two passes: the first fills both slots + # with the Release bits, then debug/ is dropped and re-primed from the + # Debug tarballs while release/ validates and is left alone. hermes-engine + # is fetched from Maven by the command itself. + - name: Prime SwiftPM artifacts (Debug + Release) + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + RN_CORE_TARBALL_PATH=/tmp/rc-release/ReactCoreRelease.xcframework.tar.gz \ + RN_DEPS_TARBALL_PATH=/tmp/deps-release/ReactNativeDependenciesRelease.xcframework.tar.gz \ + npx react-native spm download --artifacts /tmp/spm-artifacts + rm -rf /tmp/spm-artifacts/debug + RN_CORE_TARBALL_PATH=/tmp/rc-debug/ReactCoreDebug.xcframework.tar.gz \ + RN_DEPS_TARBALL_PATH=/tmp/deps-debug/ReactNativeDependenciesDebug.xcframework.tar.gz \ + npx react-native spm download --artifacts /tmp/spm-artifacts + - name: Scaffold Package.swift manifests for community dependencies + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: npx react-native spm scaffold || true + - name: Convert the app to SwiftPM + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: npx react-native spm add --deintegrate --artifacts /tmp/spm-artifacts --download skip + - name: Assert the app is on SwiftPM + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + if [[ ! -f "$XCODE_PROJECT/.spm-injected.json" ]]; then + echo "::error::spm add did not inject SwiftPM: $XCODE_PROJECT/.spm-injected.json is missing" + exit 1 + fi + if [[ -f Podfile ]] && grep -q 'use_react_native!' Podfile; then + echo "::error::spm add --deintegrate left use_react_native! in the Podfile" + exit 1 + fi + echo "SwiftPM injected in place; Podfile de-integrated." + # Under SwiftPM there is no hermes-engine pod to set HERMES_CLI_PATH, so + # resolve hermesc from the app's node_modules for react-native-xcode.sh. + - name: Resolve HERMES_CLI_PATH + if: ${{ matrix.flavor == 'Release' }} + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + HERMESC=$(node -e "const p=require('path');console.log(p.join(p.dirname(require.resolve('hermes-compiler/package.json')),'hermesc/osx-bin/hermesc'))" 2>/dev/null || true) + if [[ -z "$HERMESC" || ! -x "$HERMESC" ]]; then + echo "::error::Could not resolve hermes-compiler's hermesc from $PWD. Release builds need it to compile JS to Hermes bytecode; failing here so it is not mistaken for an xcodebuild bundling error." + exit 1 + fi + echo "HERMES_CLI_PATH=$HERMESC" >> "$GITHUB_ENV" + echo "Using hermesc at $HERMESC" + - name: Build ${{ env.XCODE_SCHEME }} (${{ matrix.flavor }}) + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + xcodebuild \ + -project "$XCODE_PROJECT" \ + -scheme "$XCODE_SCHEME" \ + -configuration "${{ matrix.flavor }}" \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath build/spm-e2e-dd \ + build + - name: Check the embedded React.framework flavor + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + PRODUCTS="build/spm-e2e-dd/Build/Products/${{ matrix.flavor }}-iphonesimulator" + if [[ ! -d "$PRODUCTS" ]]; then + echo "Skipping flavor check: no build products directory." + exit 0 + fi + BINARY=$(find "$PRODUCTS" -maxdepth 4 -path '*.app/Frameworks/React.framework/React' -type f 2>/dev/null | head -1 || true) + if [[ -z "$BINARY" ]] || ! command -v nm >/dev/null; then + echo "Skipping flavor check: no embedded React.framework binary or no nm." + exit 0 + fi + COUNT=$(nm "$BINARY" | grep -c getDebugProps || true) + echo "getDebugProps symbols in $BINARY: $COUNT" + if [[ "${{ matrix.flavor }}" == 'Debug' && "$COUNT" -eq 0 ]]; then + echo "::error::Debug build embeds a Release React.framework (expected getDebugProps symbols, found none)" + exit 1 + fi + if [[ "${{ matrix.flavor }}" == 'Release' && "$COUNT" -ne 0 ]]; then + echo "::error::Release build embeds a Debug React.framework ($COUNT getDebugProps symbols, expected none)" + exit 1 + fi diff --git a/.github/workflows/test-ios-spm-newapp.yml b/.github/workflows/test-ios-spm-newapp.yml new file mode 100644 index 000000000000..960177f659c6 --- /dev/null +++ b/.github/workflows/test-ios-spm-newapp.yml @@ -0,0 +1,162 @@ +name: Test iOS SwiftPM - New App + +permissions: + contents: read + +on: + workflow_call: + +jobs: + test: + runs-on: macos-15-large + # TODO: remove once this new lane has proven green on main. Keeps a + # brand-new SwiftPM build from hard-blocking unrelated iOS PRs. + continue-on-error: true + strategy: + fail-fast: false + matrix: + flavor: [Debug, Release] + env: + APP_IOS_DIR: private/helloworld/ios + XCODE_PROJECT: HelloWorld.xcodeproj + XCODE_SCHEME: HelloWorld + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Setup xcode + uses: ./.github/actions/setup-xcode + - name: Setup node.js + uses: ./.github/actions/setup-node + - name: Run yarn install + uses: ./.github/actions/yarn-install + - name: Set Hermes prebuilt version + shell: bash + run: node ./scripts/releases/use-hermes-prebuilt.js + - name: Run yarn install again, with the correct hermes version + uses: ./.github/actions/yarn-install + - name: Ensure CocoaPods (`spm add --deintegrate` shells out to `pod`) + shell: bash + run: pod --version || sudo gem install cocoapods --no-document + # Both flavors are needed by every matrix cell, not just the one it builds: + # `spm add` stages both flavor framework trees and lets a per-configuration + # build setting pick one at build time, so it validates the debug/ and + # release/ artifact slots together (and `--download skip` refuses an + # incomplete slot). Please don't "optimise" this down to one flavor. + - name: Download ReactCore (Debug) + uses: actions/download-artifact@v7 + with: + name: ReactCoreDebug.xcframework.tar.gz + path: /tmp/rc-debug + - name: Download ReactCore (Release) + uses: actions/download-artifact@v7 + with: + name: ReactCoreRelease.xcframework.tar.gz + path: /tmp/rc-release + - name: Download ReactNativeDependencies (Debug) + uses: actions/download-artifact@v7 + with: + name: ReactNativeDependenciesDebug.xcframework.tar.gz + path: /tmp/deps-debug + - name: Download ReactNativeDependencies (Release) + uses: actions/download-artifact@v7 + with: + name: ReactNativeDependenciesRelease.xcframework.tar.gz + path: /tmp/deps-release + - name: Download React Native package + uses: actions/download-artifact@v7 + with: + name: react-native-package + path: /tmp/react-native-tmp + # Reinstalls private/helloworld in place against the packaged react-native + # (published through the local proxy), so the app under test is what a user + # would get from npm rather than the workspace source. + - name: Prepare the HelloWorld application + shell: bash + run: node ./scripts/e2e/init-project-e2e.js --useHelloWorld --pathToLocalReactNative "/tmp/react-native-tmp/$(cat /tmp/react-native-tmp/react-native-package-version)" + # `spm download` primes both flavor slots per call and skips any slot that + # already validates, so prime in two passes: the first fills both slots + # with the Release bits, then debug/ is dropped and re-primed from the + # Debug tarballs while release/ validates and is left alone. hermes-engine + # is fetched from Maven by the command itself. + - name: Prime SwiftPM artifacts (Debug + Release) + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + RN_CORE_TARBALL_PATH=/tmp/rc-release/ReactCoreRelease.xcframework.tar.gz \ + RN_DEPS_TARBALL_PATH=/tmp/deps-release/ReactNativeDependenciesRelease.xcframework.tar.gz \ + npx react-native spm download --artifacts /tmp/spm-artifacts + rm -rf /tmp/spm-artifacts/debug + RN_CORE_TARBALL_PATH=/tmp/rc-debug/ReactCoreDebug.xcframework.tar.gz \ + RN_DEPS_TARBALL_PATH=/tmp/deps-debug/ReactNativeDependenciesDebug.xcframework.tar.gz \ + npx react-native spm download --artifacts /tmp/spm-artifacts + - name: Scaffold Package.swift manifests for community dependencies + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: npx react-native spm scaffold || true + - name: Convert the app to SwiftPM + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: npx react-native spm add --deintegrate --artifacts /tmp/spm-artifacts --download skip + - name: Assert the app is on SwiftPM + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + if [[ ! -f "$XCODE_PROJECT/.spm-injected.json" ]]; then + echo "::error::spm add did not inject SwiftPM: $XCODE_PROJECT/.spm-injected.json is missing" + exit 1 + fi + if [[ -f Podfile ]] && grep -q 'use_react_native!' Podfile; then + echo "::error::spm add --deintegrate left use_react_native! in the Podfile" + exit 1 + fi + echo "SwiftPM injected in place; Podfile de-integrated." + # Under SwiftPM there is no hermes-engine pod to set HERMES_CLI_PATH, so + # resolve hermesc from the app's node_modules for react-native-xcode.sh. + - name: Resolve HERMES_CLI_PATH + if: ${{ matrix.flavor == 'Release' }} + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + HERMESC=$(node -e "const p=require('path');console.log(p.join(p.dirname(require.resolve('hermes-compiler/package.json')),'hermesc/osx-bin/hermesc'))" 2>/dev/null || true) + if [[ -z "$HERMESC" || ! -x "$HERMESC" ]]; then + echo "::error::Could not resolve hermes-compiler's hermesc from $PWD. Release builds need it to compile JS to Hermes bytecode; failing here so it is not mistaken for an xcodebuild bundling error." + exit 1 + fi + echo "HERMES_CLI_PATH=$HERMESC" >> "$GITHUB_ENV" + echo "Using hermesc at $HERMESC" + - name: Build ${{ env.XCODE_SCHEME }} (${{ matrix.flavor }}) + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + xcodebuild \ + -project "$XCODE_PROJECT" \ + -scheme "$XCODE_SCHEME" \ + -configuration "${{ matrix.flavor }}" \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath build/spm-e2e-dd \ + build + - name: Check the embedded React.framework flavor + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + PRODUCTS="build/spm-e2e-dd/Build/Products/${{ matrix.flavor }}-iphonesimulator" + if [[ ! -d "$PRODUCTS" ]]; then + echo "Skipping flavor check: no build products directory." + exit 0 + fi + BINARY=$(find "$PRODUCTS" -maxdepth 4 -path '*.app/Frameworks/React.framework/React' -type f 2>/dev/null | head -1 || true) + if [[ -z "$BINARY" ]] || ! command -v nm >/dev/null; then + echo "Skipping flavor check: no embedded React.framework binary or no nm." + exit 0 + fi + COUNT=$(nm "$BINARY" | grep -c getDebugProps || true) + echo "getDebugProps symbols in $BINARY: $COUNT" + if [[ "${{ matrix.flavor }}" == 'Debug' && "$COUNT" -eq 0 ]]; then + echo "::error::Debug build embeds a Release React.framework (expected getDebugProps symbols, found none)" + exit 1 + fi + if [[ "${{ matrix.flavor }}" == 'Release' && "$COUNT" -ne 0 ]]; then + echo "::error::Release build embeds a Debug React.framework ($COUNT getDebugProps symbols, expected none)" + exit 1 + fi diff --git a/.github/workflows/test-ios-spm-rntester.yml b/.github/workflows/test-ios-spm-rntester.yml new file mode 100644 index 000000000000..7d56883de7af --- /dev/null +++ b/.github/workflows/test-ios-spm-rntester.yml @@ -0,0 +1,151 @@ +name: Test iOS SwiftPM - RN Tester + +permissions: + contents: read + +on: + workflow_call: + +jobs: + test: + runs-on: macos-15-large + # TODO: remove once this new lane has proven green on main. Keeps a + # brand-new SwiftPM build from hard-blocking unrelated iOS PRs. + continue-on-error: true + strategy: + fail-fast: false + matrix: + flavor: [Debug, Release] + env: + APP_IOS_DIR: packages/rn-tester + XCODE_PROJECT: RNTesterPods.xcodeproj + XCODE_SCHEME: RNTester + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Setup xcode + uses: ./.github/actions/setup-xcode + - name: Setup node.js + uses: ./.github/actions/setup-node + - name: Run yarn install + uses: ./.github/actions/yarn-install + - name: Set Hermes prebuilt version + shell: bash + run: node ./scripts/releases/use-hermes-prebuilt.js + - name: Run yarn install again, with the correct hermes version + uses: ./.github/actions/yarn-install + - name: Ensure CocoaPods (`spm add --deintegrate` shells out to `pod`) + shell: bash + run: pod --version || sudo gem install cocoapods --no-document + # Both flavors are needed by every matrix cell, not just the one it builds: + # `spm add` stages both flavor framework trees and lets a per-configuration + # build setting pick one at build time, so it validates the debug/ and + # release/ artifact slots together (and `--download skip` refuses an + # incomplete slot). Please don't "optimise" this down to one flavor. + - name: Download ReactCore (Debug) + uses: actions/download-artifact@v7 + with: + name: ReactCoreDebug.xcframework.tar.gz + path: /tmp/rc-debug + - name: Download ReactCore (Release) + uses: actions/download-artifact@v7 + with: + name: ReactCoreRelease.xcframework.tar.gz + path: /tmp/rc-release + - name: Download ReactNativeDependencies (Debug) + uses: actions/download-artifact@v7 + with: + name: ReactNativeDependenciesDebug.xcframework.tar.gz + path: /tmp/deps-debug + - name: Download ReactNativeDependencies (Release) + uses: actions/download-artifact@v7 + with: + name: ReactNativeDependenciesRelease.xcframework.tar.gz + path: /tmp/deps-release + # `spm download` primes both flavor slots per call and skips any slot that + # already validates, so prime in two passes: the first fills both slots + # with the Release bits, then debug/ is dropped and re-primed from the + # Debug tarballs while release/ validates and is left alone. hermes-engine + # is fetched from Maven by the command itself. + - name: Prime SwiftPM artifacts (Debug + Release) + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + RN_CORE_TARBALL_PATH=/tmp/rc-release/ReactCoreRelease.xcframework.tar.gz \ + RN_DEPS_TARBALL_PATH=/tmp/deps-release/ReactNativeDependenciesRelease.xcframework.tar.gz \ + npx react-native spm download --artifacts /tmp/spm-artifacts + rm -rf /tmp/spm-artifacts/debug + RN_CORE_TARBALL_PATH=/tmp/rc-debug/ReactCoreDebug.xcframework.tar.gz \ + RN_DEPS_TARBALL_PATH=/tmp/deps-debug/ReactNativeDependenciesDebug.xcframework.tar.gz \ + npx react-native spm download --artifacts /tmp/spm-artifacts + - name: Scaffold Package.swift manifests for community dependencies + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: npx react-native spm scaffold || true + - name: Convert the app to SwiftPM + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: npx react-native spm add --deintegrate --artifacts /tmp/spm-artifacts --download skip + - name: Assert the app is on SwiftPM + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + if [[ ! -f "$XCODE_PROJECT/.spm-injected.json" ]]; then + echo "::error::spm add did not inject SwiftPM: $XCODE_PROJECT/.spm-injected.json is missing" + exit 1 + fi + if [[ -f Podfile ]] && grep -q 'use_react_native!' Podfile; then + echo "::error::spm add --deintegrate left use_react_native! in the Podfile" + exit 1 + fi + echo "SwiftPM injected in place; Podfile de-integrated." + # Under SwiftPM there is no hermes-engine pod to set HERMES_CLI_PATH, so + # resolve hermesc from the app's node_modules for react-native-xcode.sh. + - name: Resolve HERMES_CLI_PATH + if: ${{ matrix.flavor == 'Release' }} + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + HERMESC=$(node -e "const p=require('path');console.log(p.join(p.dirname(require.resolve('hermes-compiler/package.json')),'hermesc/osx-bin/hermesc'))" 2>/dev/null || true) + if [[ -z "$HERMESC" || ! -x "$HERMESC" ]]; then + echo "::error::Could not resolve hermes-compiler's hermesc from $PWD. Release builds need it to compile JS to Hermes bytecode; failing here so it is not mistaken for an xcodebuild bundling error." + exit 1 + fi + echo "HERMES_CLI_PATH=$HERMESC" >> "$GITHUB_ENV" + echo "Using hermesc at $HERMESC" + - name: Build ${{ env.XCODE_SCHEME }} (${{ matrix.flavor }}) + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + xcodebuild \ + -project "$XCODE_PROJECT" \ + -scheme "$XCODE_SCHEME" \ + -configuration "${{ matrix.flavor }}" \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath build/spm-e2e-dd \ + build + - name: Check the embedded React.framework flavor + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + PRODUCTS="build/spm-e2e-dd/Build/Products/${{ matrix.flavor }}-iphonesimulator" + if [[ ! -d "$PRODUCTS" ]]; then + echo "Skipping flavor check: no build products directory." + exit 0 + fi + BINARY=$(find "$PRODUCTS" -maxdepth 4 -path '*.app/Frameworks/React.framework/React' -type f 2>/dev/null | head -1 || true) + if [[ -z "$BINARY" ]] || ! command -v nm >/dev/null; then + echo "Skipping flavor check: no embedded React.framework binary or no nm." + exit 0 + fi + COUNT=$(nm "$BINARY" | grep -c getDebugProps || true) + echo "getDebugProps symbols in $BINARY: $COUNT" + if [[ "${{ matrix.flavor }}" == 'Debug' && "$COUNT" -eq 0 ]]; then + echo "::error::Debug build embeds a Release React.framework (expected getDebugProps symbols, found none)" + exit 1 + fi + if [[ "${{ matrix.flavor }}" == 'Release' && "$COUNT" -ne 0 ]]; then + echo "::error::Release build embeds a Debug React.framework ($COUNT getDebugProps symbols, expected none)" + exit 1 + fi