Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/test-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,21 @@ jobs:
fail-on-error: true
secrets: inherit

# The templateapp lane above pins RNCore to a local tarball, which removes the
# Debug/Release script phase from the podspec. This lane is the only one that
# runs it.
test_ios_prebuilt_config_switch:
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-prebuilt-config-switch.yml
secrets: inherit

test_ios_spm_rntester:
needs:
[
Expand Down
123 changes: 123 additions & 0 deletions .github/workflows/test-ios-prebuilt-config-switch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: Test iOS Prebuilt Config Switch

permissions:
contents: read

on:
workflow_call:

jobs:
test:
runs-on: macos-15-large
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
uses: ./.github/actions/yarn-install
- name: Setup ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6.10
- name: Download React Native Package
uses: actions/download-artifact@v7
with:
name: react-native-package
path: /tmp/react-native-tmp
- name: Download ReactNativeDependencies
uses: actions/download-artifact@v7
with:
name: ReactNativeDependenciesRelease.xcframework.tar.gz
path: /tmp/third-party
- name: Download React Native Prebuilds
uses: actions/download-artifact@v7
with:
pattern: ReactCore*.xcframework.tar.gz
path: /tmp/ReactCore
merge-multiple: true
- name: Configure git
shell: bash
run: |
git config --global user.email "react-native-bot@meta.com"
git config --global user.name "React Native Bot"
- name: Create the template app
shell: bash
run: |
REACT_NATIVE_PKG=$(find /tmp/react-native-tmp -type f -name "*.tgz")

# For stable branches, we want to use the stable branch of the template
# In all the other cases, we want to use "main"
BRANCH=${{ github.ref_name }}
if ! [[ $BRANCH == *-stable* ]]; then
BRANCH=main
fi

node ./scripts/e2e/init-project-e2e.js --projectName RNTestProject --currentBranch $BRANCH --directory /tmp/RNTestProject --pathToLocalReactNative $REACT_NATIVE_PKG

# A pod that does not depend on React-Core-prebuilt has no target
# dependency ordering it behind the [RNCore] script phase, so its
# compilation can overlap with what that phase does to
# Pods/React-Core-prebuilt. Without such a pod, this lane proves nothing.
cp -R ./scripts/e2e/fixtures/RNCoreConfigSwitchProbe /tmp/RNTestProject/ios/
cd /tmp/RNTestProject/ios
awk "/^target /{print; print \" pod 'RNCoreConfigSwitchProbe', :path => './RNCoreConfigSwitchProbe'\"; next} {print}" Podfile > Podfile.probe
mv Podfile.probe Podfile
cat Podfile
- name: Install pods with prebuilt React Native Core
shell: bash
run: |
# RCT_TESTONLY_RNCORE_TARBALL_PATH is deliberately not set here: it
# takes the [RNCore] script phase out of React-Core-prebuilt.podspec,
# and that phase is what this lane exercises. So RNCore resolves the
# regular way, which needs both flavor tarballs reachable over HTTP
# under this commit's version — a version that is never published.
RN_VERSION=$(node -p "require('/tmp/RNTestProject/node_modules/react-native/package.json').version")
ARTIFACTS="/tmp/maven-mirror/com/facebook/react/react-native-artifacts/$RN_VERSION"
mkdir -p "$ARTIFACTS"
cp /tmp/ReactCore/ReactCoreDebug.xcframework.tar.gz "$ARTIFACTS/react-native-artifacts-$RN_VERSION-reactnative-core-debug.tar.gz"
cp /tmp/ReactCore/ReactCoreRelease.xcframework.tar.gz "$ARTIFACTS/react-native-artifacts-$RN_VERSION-reactnative-core-release.tar.gz"

python3 ./scripts/e2e/local-maven-mirror.py /tmp/maven-mirror 8765 &
MIRROR_PID=$!
trap 'kill $MIRROR_PID || true' EXIT
for _ in $(seq 20); do
curl -sfI -o /dev/null "http://127.0.0.1:8765/com/facebook/react/react-native-artifacts/$RN_VERSION/react-native-artifacts-$RN_VERSION-reactnative-core-debug.tar.gz" && break
sleep 1
done

cd /tmp/RNTestProject/ios
bundle install

export ENTERPRISE_REPOSITORY="http://127.0.0.1:8765"
export RCT_USE_PREBUILT_RNCORE=1
export RCT_USE_LOCAL_RN_DEP="/tmp/third-party/ReactNativeDependenciesRelease.xcframework.tar.gz"
RCT_NEW_ARCH_ENABLED=1 bundle exec pod install

# Falling back to a source build, or losing the script phase, would
# make this lane pass without testing anything — which is how #57803
# got through CI in the first place.
test -f Pods/React-Core-prebuilt/Headers/module.modulemap ||
{ echo "::error::React-Core-prebuilt was not installed from the prebuilt artifacts"; exit 1; }
grep -q replace-rncore-version.js Pods/Pods.xcodeproj/project.pbxproj ||
{ echo "::error::the [RNCore] configuration switch script phase is missing"; exit 1; }
- name: Build Release on top of a Debug install
shell: bash
run: |
cd /tmp/RNTestProject/ios

# Claim the last build was Debug, so the [RNCore] script phase swaps
# React.xcframework while the rest of the project compiles. Empty
# DerivedData makes sure the React module is precompiled during this
# build instead of being reused from an earlier one.
printf Debug > Pods/React-Core-prebuilt/.last_build_configuration

xcodebuild \
-scheme "RNTestProject" \
-workspace RNTestProject.xcworkspace \
-configuration "Release" \
-sdk "iphonesimulator" \
-destination "generic/platform=iOS Simulator" \
-derivedDataPath "/tmp/RNTestProject-config-switch"
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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.
*/

#import <Foundation/Foundation.h>

// The content does not matter: this file exists so the pod has a compile phase
// that Clang has to scan for module dependencies.
void RNCoreConfigSwitchProbe(void) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 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.

# Fixture pod for .github/workflows/test-ios-prebuilt-config-switch.yml.
#
# It declares NO dependencies on purpose. React Native activates the prebuilt
# React module map on every pod target, but only the pods that depend on
# React-Core-prebuilt are ordered behind its Debug/Release script phase. A pod
# without that edge is what lets its compilation overlap with the script phase,
# which is what regressed in #57803. Adding a dependency here would silence the
# lane instead of fixing anything.
Pod::Spec.new do |spec|
spec.name = 'RNCoreConfigSwitchProbe'
spec.version = '1.0.0'
spec.summary = 'Compiles one file, depends on nothing, and fails when the prebuilt React module map disappears mid-build.'
spec.homepage = 'https://reactnative.dev/'
spec.license = { :type => 'MIT' }
spec.author = 'Meta Platforms, Inc. and its affiliates'
spec.platform = :ios, '15.1'
spec.source = { :path => '.' }
spec.source_files = 'RNCoreConfigSwitchProbe.m'
end
43 changes: 43 additions & 0 deletions scripts/e2e/local-maven-mirror.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
# 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.

"""Serves a directory as a Maven repository and redirects misses to Maven Central.

CocoaPods only consumes the prebuilt React Native Core artifacts when they are
reachable over HTTP (see packages/react-native/scripts/cocoapods/rncore.rb), and
a CI run's version is never published. Point ENTERPRISE_REPOSITORY at this
server to serve locally built artifacts under their published names while
Hermes and everything else keep resolving from Maven Central.

Usage: local-maven-mirror.py <directory> <port>
"""

import functools
import os
import sys
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer

UPSTREAM = "https://repo1.maven.org/maven2"


class MirrorRequestHandler(SimpleHTTPRequestHandler):
def send_head(self):
if os.path.isfile(self.translate_path(self.path)):
return super().send_head()
self.send_response(302)
self.send_header("Location", UPSTREAM + self.path)

Check warning

Code scanning / CodeQL

HTTP Response Splitting Medium

This HTTP header is constructed from a
user-provided value
.
self.end_headers()
return None


def main(directory, port):
handler = functools.partial(MirrorRequestHandler, directory=directory)
print(f"Mirroring {directory} on port {port}, redirecting misses to {UPSTREAM}")
ThreadingHTTPServer(("127.0.0.1", port), handler).serve_forever()


if __name__ == "__main__":
main(sys.argv[1], int(sys.argv[2]))
Loading