diff --git a/.github/workflow-scripts/assert-spm-deinit.sh b/.github/workflow-scripts/assert-spm-deinit.sh new file mode 100755 index 000000000000..b8bd6f5072a3 --- /dev/null +++ b/.github/workflow-scripts/assert-spm-deinit.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +# Checks that `npx react-native spm deinit` left an iOS app with only its +# CocoaPods files. It checks only what deinit removes; `pod install` restores +# the CocoaPods references, so they are not checked here. +# +# It assumes the app had no Swift packages before `spm add`: any +# packageReferences or packageProductDependencies left after deinit fail the +# check. Do not use it on an app that already has Swift packages. +# +# Usage: +# assert-spm-deinit.sh record +# Run before `spm add`. Prints PODFILE_SHA256 and GITIGNORE_SHA256, and +# appends them to $GITHUB_ENV when it is set. +# assert-spm-deinit.sh check +# Run after `spm deinit`, with the values that record printed. + +usage() { + echo "Usage: $0 record " + echo " $0 check " + exit 2 +} + +sha256_or_absent() { + if [ -f "$1" ]; then + shasum -a 256 "$1" | cut -d ' ' -f 1 + else + echo absent + fi +} + +record() { + local ios_dir=$1 + if [ ! -f "$ios_dir/Podfile" ]; then + echo "::error::$ios_dir/Podfile is missing. Record the checksums while the app is still on CocoaPods." + exit 1 + fi + + local values + values="PODFILE_SHA256=$(sha256_or_absent "$ios_dir/Podfile") +GITIGNORE_SHA256=$(sha256_or_absent "$ios_dir/.gitignore")" + echo "$values" + if [ -n "$GITHUB_ENV" ]; then + echo "$values" >> "$GITHUB_ENV" + fi +} + +check() { + local ios_dir=$1 + local xcodeproj="$ios_dir/$2" + local expected_podfile_sha256=$3 + local expected_gitignore_sha256=$4 + local pbxproj="$xcodeproj/project.pbxproj" + local failed=0 + + fail() { + echo "::error::$1" + failed=1 + } + + local actual_podfile_sha256 + actual_podfile_sha256=$(sha256_or_absent "$ios_dir/Podfile") + if [ "$actual_podfile_sha256" != "$expected_podfile_sha256" ]; then + fail "Podfile changed: sha256 is '$actual_podfile_sha256', expected '$expected_podfile_sha256'." + fi + + local actual_gitignore_sha256 + actual_gitignore_sha256=$(sha256_or_absent "$ios_dir/.gitignore") + if [ "$actual_gitignore_sha256" != "$expected_gitignore_sha256" ]; then + fail ".gitignore changed: sha256 is '$actual_gitignore_sha256', expected '$expected_gitignore_sha256'." + fi + + if [ -e "$xcodeproj/.spm-injected.json" ]; then + fail "$xcodeproj/.spm-injected.json is still there." + fi + + if [ -e "$ios_dir/ReactNativeSPM" ]; then + fail "$ios_dir/ReactNativeSPM is still there." + fi + + if [ ! -f "$pbxproj" ]; then + fail "$pbxproj is missing." + else + local injected + for injected in \ + XCLocalSwiftPackageReference \ + XCSwiftPackageProductDependency \ + packageReferences \ + packageProductDependencies \ + ReactNativeSPM \ + "Sync SPM Autolinking" \ + "Embed React Native Flavored Frameworks"; do + if grep -qF "$injected" "$pbxproj"; then + fail "$pbxproj still contains '$injected'." + fi + done + fi + + local scheme + for scheme in "$xcodeproj"/xcshareddata/xcschemes/*.xcscheme; do + if [ -f "$scheme" ] && grep -qF 'title = "Sync SPM Autolinking"' "$scheme"; then + fail "$scheme still has the Sync SPM Autolinking pre-action." + fi + done + + if [ $failed -ne 0 ]; then + exit 1 + fi + echo "spm deinit removed every SwiftPM change." +} + +case "$1" in + record) + [ $# -eq 2 ] || usage + record "$2" + ;; + check) + [ $# -eq 5 ] || usage + check "$2" "$3" "$4" "$5" + ;; + *) + usage + ;; +esac diff --git a/.github/workflows/test-ios-spm-newapp.yml b/.github/workflows/test-ios-spm-newapp.yml index 5607e8b94804..b21f653f199a 100644 --- a/.github/workflows/test-ios-spm-newapp.yml +++ b/.github/workflows/test-ios-spm-newapp.yml @@ -46,6 +46,14 @@ jobs: uses: ./.github/actions/setup-spm-artifacts with: working-directory: ${{ env.APP_IOS_DIR }} + # The `spm deinit` round trip runs in the Debug cell only: it does not + # depend on the flavor. + - name: Record HelloWorld before SwiftPM + if: matrix.flavor == 'Debug' + shell: bash + run: | + ./.github/workflow-scripts/assert-spm-deinit.sh record "$APP_IOS_DIR" + cp -R "$APP_IOS_DIR" "$RUNNER_TEMP/ios-before-spm" # HelloWorld is a CocoaPods project in git. CI converts it to SwiftPM on every run. - name: Convert HelloWorld to SwiftPM shell: bash @@ -100,3 +108,47 @@ jobs: echo "::error::Release build embeds a Debug React.framework ($COUNT getDebugProps symbols, expected none)" exit 1 fi + - name: Take HelloWorld back to CocoaPods + if: matrix.flavor == 'Debug' + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: npx react-native spm deinit + # Informational only. Expected differences include the CocoaPods + # references and the .xcworkspace, which `pod install` restores, and the + # JavaScriptCore.framework reference and empty Pods group, which deinit + # does not restore on purpose. + - name: Show what changed since before SwiftPM + if: ${{ !cancelled() && matrix.flavor == 'Debug' }} + shell: bash + run: diff -r -x build "$RUNNER_TEMP/ios-before-spm" "$APP_IOS_DIR" || true + - name: Check that only the CocoaPods files are left + if: matrix.flavor == 'Debug' + shell: bash + run: ./.github/workflow-scripts/assert-spm-deinit.sh check "$APP_IOS_DIR" "$XCODE_PROJECT" "$PODFILE_SHA256" "$GITIGNORE_SHA256" + - name: Setup ruby + if: matrix.flavor == 'Debug' + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6.10 + # The tarballs are the Debug ones that setup-spm-artifacts downloaded. + - name: Install the CocoaPods dependencies + if: matrix.flavor == 'Debug' + shell: bash + working-directory: private/helloworld + run: | + export RCT_USE_LOCAL_RN_DEP=/tmp/deps-debug/ReactNativeDependenciesDebug.xcframework.tar.gz + export RCT_TESTONLY_RNCORE_TARBALL_PATH=/tmp/rc-debug/ReactCoreDebug.xcframework.tar.gz + yarn bootstrap ios | cat + - name: Build ${{ env.XCODE_SCHEME }} with CocoaPods (Debug) + if: matrix.flavor == 'Debug' + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + xcodebuild \ + -workspace "${XCODE_PROJECT%.xcodeproj}.xcworkspace" \ + -scheme "$XCODE_SCHEME" \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath build/cocoapods-e2e-dd \ + build