Skip to content
Merged
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
5 changes: 4 additions & 1 deletion .github/workflows/buildIOS.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,10 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
RESULT=$(./node_modules/.bin/bun scripts/artifacts-utils/resolve-artifacts.ts --platform=ios --hybrid=true --new-dot-root="$GITHUB_WORKSPACE")
# pod install resolves the same artifact through patched_ios_artifacts.rb, which picks the Hermes tag file from
# RCT_HERMES_V1_ENABLED. HybridApp pins that to '1' inside Mobile-Expensify/iOS/Podfile (invisible to this shell),
# so the cache key must assume Hermes V1 to stay in sync with what pod install downloads.
RESULT=$(./node_modules/.bin/bun scripts/artifacts-utils/resolve-artifacts.ts --platform=ios --hybrid=true --new-dot-root="$GITHUB_WORKSPACE" --hermes-version-file="$GITHUB_WORKSPACE/node_modules/react-native/sdks/.hermesv1version")
VERSION=$(echo "$RESULT" | jq -r '.version // "source"')
if [ -z "$VERSION" ]; then
echo "::error::Could not resolve react-native artifact version from resolve-artifacts.ts output: $RESULT"
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/publishReactNativeAndroidArtifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ jobs:
with:
IS_HYBRID_BUILD: ${{ matrix.is_hybrid }}

# react-android is compiled against the headers of the Hermes that hermes-engine builds for this release.
# Recording its version in the POM lets the resolver reject an artifact whose Hermes differs from the one the
# consuming app links.
- name: Resolve Hermes version
id: hermes
run: echo "VERSION=$(./scripts/artifacts-utils/get-hermes-version.sh)" >> "$GITHUB_OUTPUT"

- name: Get Java version
id: get-java-version
uses: ./.github/actions/composite/getJavaVersion
Expand Down Expand Up @@ -97,6 +104,7 @@ jobs:
echo "Starting artifacts build for ${{ matrix.is_hybrid == 'true' && 'HybridApp' || 'NewDot Standalone' }}"
echo "Version: ${{ env.PATCHED_VERSION }}"
echo "Patches hash: ${{ env.PATCHES_HASH }}"
echo "Hermes version: ${{ env.HERMES_VERSION }}"
export ORG_GRADLE_PROJECT_reactNativeArchitectures="armeabi-v7a,arm64-v8a,x86,x86_64"

# Exclude ktfmt and test tasks from the included react-native build
Expand All @@ -117,6 +125,7 @@ jobs:
IS_HYBRID_BUILD: ${{ matrix.is_hybrid }}
PATCHED_VERSION: ${{ steps.getNewPatchedVersion.outputs.NEW_PATCHED_VERSION }}
PATCHES_HASH: ${{ steps.computePatchesHash.outputs.PATCHES_HASH }}
HERMES_VERSION: ${{ steps.hermes.outputs.VERSION }}
CMAKE_VERSION: 3.31.6

- name: Announce failed workflow in Slack
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/publishReactNativeArtifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ on:
- .github/workflows/publishReactNativeAndroidArtifacts.yml
- .github/actions/composite/fetchReactNativePrebuild/action.yml
- scripts/artifacts-utils/ios/publish/**
- scripts/artifacts-utils/get-hermes-version.sh

jobs:
verifyPatches:
Expand Down Expand Up @@ -134,7 +135,8 @@ jobs:
.github/workflows/publishReactNativeiOSArtifacts.yml \
.github/workflows/publishReactNativeAndroidArtifacts.yml \
.github/actions/composite/fetchReactNativePrebuild/action.yml \
scripts/artifacts-utils/ios/publish; then
scripts/artifacts-utils/ios/publish \
scripts/artifacts-utils/get-hermes-version.sh; then
echo "DID_WORKFLOWS_CHANGE=false" >> "$GITHUB_OUTPUT"
else
echo "DID_WORKFLOWS_CHANGE=true" >> "$GITHUB_OUTPUT"
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/publishReactNativeiOSArtifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:

- name: Resolve Hermes version
id: hermes
run: echo "VERSION=$(sed 's/^hermes-v\{0,1\}//' node_modules/react-native/sdks/.hermesv1version)" >> "$GITHUB_OUTPUT"
run: echo "VERSION=$(./scripts/artifacts-utils/get-hermes-version.sh)" >> "$GITHUB_OUTPUT"

- name: Fetch prebuild files from upstream RN
uses: ./.github/actions/composite/fetchReactNativePrebuild
Expand Down Expand Up @@ -219,6 +219,12 @@ jobs:
echo "PATCHES_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT"
fi

# The slices were compiled against this Hermes (see the buildSlice job). Recording it in the POM lets the
# resolver reject an artifact whose Hermes differs from the one the consuming app links.
- name: Resolve Hermes version
id: hermes
run: echo "VERSION=$(./scripts/artifacts-utils/get-hermes-version.sh)" >> "$GITHUB_OUTPUT"

- name: Determine new patched RN version
id: getNewPatchedVersion
run: echo "NEW_PATCHED_VERSION=$(./.github/scripts/getNewPatchedRNVersion.sh)" >> "$GITHUB_OUTPUT"
Expand All @@ -235,12 +241,13 @@ jobs:
merge-multiple: true

- name: Publish to GitHub Packages Maven
run: ./android/gradlew -p scripts/artifacts-utils/ios/publish publish -PartifactsDir="/tmp/ios-artifacts" -PpatchedVersion="${PATCHED_VERSION}" -PpatchesHash="${PATCHES_HASH}" -PpackageName="${PACKAGE_NAME}"
run: ./android/gradlew -p scripts/artifacts-utils/ios/publish publish -PartifactsDir="/tmp/ios-artifacts" -PpatchedVersion="${PATCHED_VERSION}" -PpatchesHash="${PATCHES_HASH}" -PhermesVersion="${HERMES_VERSION}" -PpackageName="${PACKAGE_NAME}"
env:
GH_PUBLISH_ACTOR: ${{ github.actor }}
GH_PUBLISH_TOKEN: ${{ github.token }}
PATCHED_VERSION: ${{ steps.getNewPatchedVersion.outputs.NEW_PATCHED_VERSION }}
PATCHES_HASH: ${{ steps.computePatchesHash.outputs.PATCHES_HASH }}
HERMES_VERSION: ${{ steps.hermes.outputs.VERSION }}
PACKAGE_NAME: ${{ matrix.is_hybrid == 'true' && 'react-hybrid' || 'react-standalone' }}

- name: Announce failed workflow in Slack
Expand Down
3 changes: 3 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,9 @@
"healthcheck",
"helpdot",
"helpsite",
"hermestag",
"hermesv",
"hermesversion",
"hexcode",
"hibob",
"hrefs",
Expand Down
2 changes: 0 additions & 2 deletions ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,6 @@ target 'NewExpensify' do

# ENV Variable enables/disables TurboModules
ENV['RCT_NEW_ARCH_ENABLED'] = '1';
# ENV Variable enables/disables Hermes V1
ENV['RCT_HERMES_V1_ENABLED'] = '1';


use_react_native!(
Expand Down
2 changes: 1 addition & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4134,6 +4134,6 @@ SPEC CHECKSUMS:
VisionCamera: 30b358b807324c692064f78385e9a732ce1bebfe
Yoga: c30c859b7e9b75e547b600b486fe33165f60de00

PODFILE CHECKSUM: c937d1b7b774ce019c0c848e4ad1e850f08c0e90
PODFILE CHECKSUM: cdbd46f6c89c7123ca4821dc03f66599047601f8

COCOAPODS: 1.16.2
2 changes: 1 addition & 1 deletion patches/react-native/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@

### [react-native+0.86.0+017+publish-gradle.patch](react-native+0.86.0+017+publish-gradle.patch)

- Reason: This patch customizes the Gradle publishing script to allow publishing our custom React Native artifacts to GitHub Packages.
- Reason: This patch customizes the Gradle publishing script to allow publishing our custom React Native artifacts to GitHub Packages. The POM records the patches hash and the Hermes version the artifact was compiled against, which `scripts/artifacts-utils/lib/artifactsResolver.ts` matches before consuming an artifact.
- Upstream PR/issue: 🛑
- E/App issue: 🛑
- PR Introducing Patch: https://github.com/Expensify/App/pull/59738
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ new file mode 100644
index 0000000..8bfcb73
Binary files /dev/null and b/node_modules/react-native/ReactAndroid/.DS_Store differ
diff --git a/node_modules/react-native/ReactAndroid/publish.gradle b/node_modules/react-native/ReactAndroid/publish.gradle
index 32287a7..5607be3 100644
index 32287a7..77f9452 100644
--- a/node_modules/react-native/ReactAndroid/publish.gradle
+++ b/node_modules/react-native/ReactAndroid/publish.gradle
@@ -13,7 +13,11 @@ def signingKey = findProperty("SIGNING_KEY")
Expand Down Expand Up @@ -40,12 +40,13 @@ index 32287a7..5607be3 100644
}

pom {
@@ -39,10 +45,14 @@ publishing {
@@ -39,10 +45,15 @@ publishing {
description = "A framework for building native apps with React"
url = "https://github.com/facebook/react-native"

+ properties = [
+ "patchesHash": System.getenv("PATCHES_HASH"),
+ "hermesVersion": System.getenv("HERMES_VERSION"),
+ ]
+
developers {
Expand All @@ -57,7 +58,7 @@ index 32287a7..5607be3 100644
}
}

@@ -65,8 +75,11 @@ publishing {
@@ -65,8 +76,11 @@ publishing {

repositories {
maven {
Expand Down
10 changes: 8 additions & 2 deletions scripts/artifacts-utils/android/ExpensiUtils.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,17 @@ ext.ExpensiUtils = new Object() {
return [output: process.in.text?.trim(), exitCode: process.exitValue()]
}

// The Hermes tag file react-native builds (and we publish) against for the given flag. Mirrors hermesVersionProvider
// in ReactAndroid/hermes-engine/build.gradle.kts, which is project-phase code the settings phase cannot reach.
String hermesVersionFile(String newDotRootDir, boolean hermesV1Enabled) {
return "${newDotRootDir}/node_modules/react-native/sdks/${hermesV1Enabled ? '.hermesv1version' : '.hermesversion'}"
}

// Returns [buildFromSource, version, githubUsername, githubToken]; any failure -> build from source.
Map resolveArtifacts(String newDotRootDir, boolean isHybrid) {
Map resolveArtifacts(String newDotRootDir, boolean isHybrid, String hermesVersionFile) {
def cmd = [
"bun", "${newDotRootDir}/scripts/artifacts-utils/resolve-artifacts.ts",
"--platform=android", "--hybrid=${isHybrid}", "--new-dot-root=${newDotRootDir}"
"--platform=android", "--hybrid=${isHybrid}", "--new-dot-root=${newDotRootDir}", "--hermes-version-file=${hermesVersionFile}"
]
try {
def result = runCommand(cmd, 120, new File(newDotRootDir))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ settings.extensions.configure(PatchedArtifactsConfig) { config ->
config.packageName = getProperty('patchedArtifacts.packageName')

def isHybrid = config.packageName == 'react-hybrid'
def resolution = ExpensiUtils.resolveArtifacts(getNewDotRootDir().toString(), isHybrid)
// Same default as the react-native Gradle plugin: Hermes V1 unless `hermesV1Enabled` (gradle.properties) opts out.
// The resolver only accepts an artifact compiled against the Hermes this flag makes the build link.
def hermesV1Enabled = hasProperty('hermesV1Enabled') ? getProperty('hermesV1Enabled').toString().toBoolean() : true
def newDotRootDir = getNewDotRootDir().toString()
def resolution = ExpensiUtils.resolveArtifacts(newDotRootDir, isHybrid, ExpensiUtils.hermesVersionFile(newDotRootDir, hermesV1Enabled))
config.version = resolution.version
config.buildFromSource = resolution.buildFromSource

Expand Down
21 changes: 21 additions & 0 deletions scripts/artifacts-utils/get-hermes-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
Comment thread
mateuuszzzzz marked this conversation as resolved.

# Prints the Hermes version our prebuilt artifacts are compiled against (e.g. `250829098.0.14`), read from
# `sdks/hermes-engine/version.properties` the way upstream's own prebuild workflow does (prebuild-ios-core.yml in
# facebook/react-native). The publish workflows record it in the POM, and the artifacts resolver refuses an artifact
# whose Hermes differs from the one the app links, so an artifact built against the wrong ABI is never used silently.

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
readonly SCRIPT_DIR
source "$SCRIPT_DIR/../shellUtils.sh"

NEW_DOT_ROOT="${1:-$SCRIPT_DIR/../..}"
readonly NEW_DOT_ROOT
readonly HERMES_VERSION_FILE="$NEW_DOT_ROOT/node_modules/react-native/sdks/hermes-engine/version.properties"

if [[ ! -f "$HERMES_VERSION_FILE" ]]; then
error "Hermes version file not found: $HERMES_VERSION_FILE"
exit 1
fi

sed -n 's/^HERMES_V1_VERSION_NAME=//p' "$HERMES_VERSION_FILE"
Comment thread
mateuuszzzzz marked this conversation as resolved.
Comment thread
mateuuszzzzz marked this conversation as resolved.
5 changes: 4 additions & 1 deletion scripts/artifacts-utils/ios/patched_ios_artifacts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,12 @@ def self.download_and_verify(url, destination, github_token)
end

def self.resolve(is_hybrid)
react_native_path = File.join(NEW_DOT_ROOT, 'node_modules/react-native')
require_relative File.join(react_native_path, 'sdks/hermes-engine/hermes-utils.rb')
hermes_version_file = hermestag_file(react_native_path)
cmd = [
'bun', File.join(NEW_DOT_ROOT, 'scripts/artifacts-utils/resolve-artifacts.ts'),
'--platform=ios', "--hybrid=#{is_hybrid}", "--new-dot-root=#{NEW_DOT_ROOT}"
'--platform=ios', "--hybrid=#{is_hybrid}", "--new-dot-root=#{NEW_DOT_ROOT}", "--hermes-version-file=#{hermes_version_file}"
]
# stdout is pure JSON; the resolver logs to stderr.
output = IO.popen(cmd, chdir: NEW_DOT_ROOT, &:read)
Expand Down
2 changes: 2 additions & 0 deletions scripts/artifacts-utils/ios/publish/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ apply plugin: 'maven-publish'
def artifactsDir = findProperty('artifactsDir') ?: error('artifactsDir property is required')
def patchedVersion = findProperty('patchedVersion') ?: error('patchedVersion property is required')
def patchesHash = findProperty('patchesHash') ?: error('patchesHash property is required')
def hermesVersion = findProperty('hermesVersion') ?: error('hermesVersion property is required')
def packageName = findProperty('packageName') ?: error('packageName property is required')

configurations.maybeCreate('iosArtifacts')
Expand Down Expand Up @@ -50,6 +51,7 @@ publishing {

properties = [
'patchesHash': patchesHash,
'hermesVersion': hermesVersion,
]

developers {
Expand Down
51 changes: 43 additions & 8 deletions scripts/artifacts-utils/lib/artifactsResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {getCredentials as getCredentialsFromCLI} from './githubCLI';

/**
* Shared resolver for Expensify's patched React Native prebuilt artifacts.
* Resolves which artifact version to use by matching the local patches hash
* against the `patchesHash` recorded in each candidate's Maven POM. Consumed via
* Resolves which artifact version to use by matching the local patches hash and Hermes version
* against the `patchesHash` and `hermesVersion` recorded in each candidate's Maven POM. Consumed via
* `resolve-artifacts.ts` by Gradle (Android) and `patched_ios_artifacts.rb` (iOS).
*/

Expand All @@ -30,8 +30,16 @@ type ResolveOptions = {
packageName: string;
newDotRoot: string;
isHybrid: boolean;
/**
* React Native's Hermes tag file for the engine this build links (`sdks/.hermesv1version`, or
* `sdks/.hermesversion` with Hermes V1 disabled). The native caller picks it, since it owns the V1 flag.
*/
hermesVersionFile: string;
};

/** What a published artifact was built from, as recorded in its POM's `<properties>`. A property the POM does not carry is null. */
type PomProperties = {patchesHash: string | null; hermesVersion: string | null};

/** No prebuilt match (or no credentials): the caller builds react-native from source. Carries no secrets. */
type SourceBuild = {
buildFromSource: true;
Expand Down Expand Up @@ -146,6 +154,24 @@ function computePatchesHash(newDotRoot: string, isHybrid: boolean): string {
return execFileSync('bash', args, {encoding: 'utf8'}).trim();
}

/**
* The Hermes version this build links, read from its tag file without the tag prefix, so `hermes-v250829098.0.14`
* becomes `250829098.0.14`. That is the form the publish workflows record (`get-hermes-version.sh`, which reads
* HERMES_V1_VERSION_NAME from `version.properties` like upstream does). The artifacts are not bundled with a Hermes
* engine, but their React code is compiled against the headers of one, so an artifact is only usable when its
* recorded Hermes version is the one this build links.
*/
function getLocalHermesVersion(hermesVersionFile: string): string {
const version = fs
.readFileSync(hermesVersionFile, 'utf8')
.trim()
.replace(/^hermes-v?/, '');
if (!version) {
throw new Error(`Could not read the Hermes version from ${hermesVersionFile}`);
}
return version;
}

/** Published versions of the package that were built from the given react-native version. Requires `initGithubClient`. */
async function getArtifactsCandidates(packageName: string, artifactId: string, rnVersion: string): Promise<string[]> {
/* eslint-disable @typescript-eslint/naming-convention -- GitHub REST API params are snake_case */
Expand All @@ -158,22 +184,31 @@ async function getArtifactsCandidates(packageName: string, artifactId: string, r
return versions.map((version) => version.name).filter((name) => name.startsWith(rnVersion));
}

async function getRemotePatchesHash(packageName: string, artifactId: string, version: string, githubToken: string): Promise<string | null> {
function readPomProperty(pom: string, name: string): string | null {
return pom.match(new RegExp(`<${name}>([^<]+)</${name}>`))?.[1]?.trim() ?? null;
}

async function getRemotePomProperties(packageName: string, artifactId: string, version: string, githubToken: string): Promise<PomProperties> {
const pom = await fetchWithToken(`${getArtifactUrlPrefix(packageName, artifactId, version)}.pom`, githubToken);
return pom.match(/<patchesHash>([^<]+)<\/patchesHash>/)?.[1]?.trim() ?? null;
return {patchesHash: readPomProperty(pom, 'patchesHash'), hermesVersion: readPomProperty(pom, 'hermesVersion')};
}

/** Returns null when no published artifact was built from the local patches — a legitimate result, not a failure. */
/** Returns null when no published artifact was built from the local patches and Hermes. That is a legitimate result, not a failure. */
async function findMatchingArtifactsVersion(options: ResolveOptions, artifactId: string, githubToken: string): Promise<string | null> {
const {packageName, newDotRoot, isHybrid} = options;
const {packageName, newDotRoot, isHybrid, hermesVersionFile} = options;
const localPatchesHash = computePatchesHash(newDotRoot, isHybrid);
const localHermesVersion = getLocalHermesVersion(hermesVersionFile);
const rnVersion = getReactNativeVersion(newDotRoot);
const candidates = await getArtifactsCandidates(packageName, artifactId, rnVersion);
for (const candidate of candidates) {
const remoteHash = await getRemotePatchesHash(packageName, artifactId, candidate, githubToken);
if (remoteHash === localPatchesHash) {
const remote = await getRemotePomProperties(packageName, artifactId, candidate, githubToken);
if (remote.patchesHash !== localPatchesHash) {
continue;
}
if (remote.hermesVersion === localHermesVersion) {
return candidate;
}
logWarn(`${LOG_PREFIX} Skipping ${packageName}:${candidate}: built against Hermes ${remote.hermesVersion ?? 'unknown'}, but this build links Hermes ${localHermesVersion}.`);
}
return null;
}
Expand Down
12 changes: 10 additions & 2 deletions scripts/artifacts-utils/resolve-artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import resolveArtifacts from './lib/artifactsResolver';
*
* Usage:
* bun scripts/artifacts-utils/resolve-artifacts.ts \
* --platform=ios --hybrid=true --new-dot-root=.
* --platform=ios --hybrid=true --new-dot-root=. \
* --hermes-version-file=node_modules/react-native/sdks/.hermesv1version
*
* Prints the result as JSON to stdout (logs go to stderr) and always exits 0.
*/
const args = parseCommandLineArguments();
const platform = args.platform;
const isHybrid = args.hybrid === 'true';
const hermesVersionFile = args['hermes-version-file'];
const packageName = isHybrid ? 'react-hybrid' : 'react-standalone';

if (platform !== 'ios' && platform !== 'android') {
Expand All @@ -22,7 +24,13 @@ if (platform !== 'ios' && platform !== 'android') {
process.exit(0);
}

const options = {packageName, newDotRoot: args['new-dot-root'] ?? '.', isHybrid};
if (!hermesVersionFile) {
process.stderr.write('[PatchedArtifacts] Missing --hermes-version-file (the Hermes tag file this build links); building from source.\n');
process.stdout.write(JSON.stringify({buildFromSource: true, version: null, packageName, artifactId: ''}));
process.exit(0);
}

const options = {packageName, newDotRoot: args['new-dot-root'] ?? '.', isHybrid, hermesVersionFile};
const resolution = platform === 'ios' ? resolveArtifacts({...options, platform: 'ios'}) : resolveArtifacts({...options, platform: 'android'});

resolution
Expand Down
Loading
Loading