Skip to content
Open
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
8 changes: 4 additions & 4 deletions .github/workflows/prebuild-ios-dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
uses: actions/cache/restore@v5
with:
path: packages/react-native/third-party/.build/Build/Products
key: v3-ios-dependencies-slice-folder-${{ matrix.slice }}-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js') }}
key: v3-ios-dependencies-slice-folder-${{ matrix.slice }}-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js', 'scripts/releases/ios-prebuild/build.js') }}
- name: Setup xcode
if: steps.restore-slice-folder.outputs.cache-hit != 'true'
uses: ./.github/actions/setup-xcode
Expand Down Expand Up @@ -101,7 +101,7 @@ jobs:
uses: actions/cache/save@v5
if: ${{ github.ref == 'refs/heads/main' }} # To avoid that the cache explode
with:
key: v3-ios-dependencies-slice-folder-${{ matrix.slice }}-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js') }}
key: v3-ios-dependencies-slice-folder-${{ matrix.slice }}-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js', 'scripts/releases/ios-prebuild/build.js') }}
enableCrossOsArchive: true
path: |
packages/react-native/third-party/.build/Build/Products
Expand Down Expand Up @@ -130,7 +130,7 @@ jobs:
with:
path: |
packages/react-native/third-party/
key: v5-ios-dependencies-xcframework-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js', 'scripts/releases/ios-prebuild/compose-framework.js', 'packages/react-native/scripts/ios-prebuild/headers-xcframework.js', 'packages/react-native/scripts/ios-prebuild/headers-spec.js') }}
key: v5-ios-dependencies-xcframework-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js', 'scripts/releases/ios-prebuild/build.js', 'scripts/releases/ios-prebuild/compose-framework.js', 'packages/react-native/scripts/ios-prebuild/headers-xcframework.js', 'packages/react-native/scripts/ios-prebuild/headers-spec.js') }}
# If cache hit, we already have our binary. We don't need to do anything.
- name: Yarn Install
if: steps.restore-xcframework.outputs.cache-hit != 'true'
Expand Down Expand Up @@ -204,4 +204,4 @@ jobs:
packages/react-native/third-party/ReactNativeDependencies${{ matrix.flavor }}.xcframework.tar.gz
packages/react-native/third-party/ReactNativeDependenciesHeaders${{ matrix.flavor }}.xcframework.tar.gz
packages/react-native/third-party/ReactNativeDependencies${{ matrix.flavor }}.framework.dSYM.tar.gz
key: v5-ios-dependencies-xcframework-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js', 'scripts/releases/ios-prebuild/compose-framework.js', 'packages/react-native/scripts/ios-prebuild/headers-xcframework.js', 'packages/react-native/scripts/ios-prebuild/headers-spec.js') }}
key: v5-ios-dependencies-xcframework-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js', 'scripts/releases/ios-prebuild/build.js', 'scripts/releases/ios-prebuild/compose-framework.js', 'packages/react-native/scripts/ios-prebuild/headers-xcframework.js', 'packages/react-native/scripts/ios-prebuild/headers-spec.js') }}
4 changes: 3 additions & 1 deletion packages/react-native/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ let yoga = RNTarget(
name: .yoga,
path: "ReactCommon/yoga",
excludedPaths: ["test"],
publicHeadersPath: "yoga"
publicHeadersPath: "yoga",
// Android builds Yoga at -O2 in every build type, Debug included, so match it.
defines: [.unsafeFlags(["-O2"])]
)

// React-oscompat.podspec
Expand Down
51 changes: 51 additions & 0 deletions packages/react-native/scripts/cocoapods/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,57 @@ def self.add_ndebug_flag_to_pods_in_release(installer)
end
end

REACT_NATIVE_THIRD_PARTY_PODS = [
'RCT-Folly',
'glog',
'boost',
'DoubleConversion',
'fmt',
'fast_float',
'SocketRocket',
].freeze

# Generated by React Native into the app's build folder, so not under react_native_path.
REACT_NATIVE_GENERATED_PODS = [
'ReactCodegen',
'ReactAppDependencyProvider',
].freeze

ALWAYS_OPTIMIZED_PODS = [
'Yoga',
'glog',
].freeze

# Xcode builds Release at -Os by default. Android builds React Native and its
# third-party dependencies at -O2 in Release, and Yoga and glog at -O2 in Debug
# too, so match it. Hermes keeps its own MinSizeRel build on both platforms, and
# pods outside React Native are left alone.
def self.set_optimization_level_for_react_native_pods(installer, react_native_path)
Pod::UI.puts("Setting GCC_OPTIMIZATION_LEVEL to 2 for React Native pods in Release, and for #{ALWAYS_OPTIMIZED_PODS.join(', ')} in all configurations")
rn_real_path = File.realpath(File.expand_path(react_native_path, Pod::Config.instance.installation_root.to_s))

installer.target_installation_results.pod_target_installation_results.each do |_, target_installation_result|
pod_name = target_installation_result.target.pod_name
next unless self.react_native_pod?(installer.sandbox, pod_name, rn_real_path)

always_optimized = ALWAYS_OPTIMIZED_PODS.include?(pod_name)
target_installation_result.native_target.build_configurations.each do |config|
next unless always_optimized || config.type == :release
config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '2'
end
end
end
Comment thread
cipolleschi marked this conversation as resolved.

def self.react_native_pod?(sandbox, pod_name, rn_real_path)
return false if pod_name == 'hermes-engine'
return true if REACT_NATIVE_THIRD_PARTY_PODS.include?(pod_name)
return true if REACT_NATIVE_GENERATED_PODS.include?(pod_name)
return false unless sandbox.local?(pod_name)

pod_real_path = File.realpath(sandbox.pod_dir(pod_name).to_s)
pod_real_path == rn_real_path || pod_real_path.start_with?(rn_real_path + File::SEPARATOR)
end

def self.add_flag_to_map_with_inheritance(map, field, flag)
if map[field] == nil
map[field] = "$(inherited)" + flag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ let package = Package(
path: "ReactCodegen",
exclude: ["ReactCodegen.podspec"],
publicHeadersPath: ".",
cSettings: [.headerSearchPath("headers")],
// Release builds at -O2 to match CocoaPods and Android; Xcode defaults
// Release to -Os. cSettings covers .m sources, cxxSettings .mm/.cpp.
cSettings: [
.headerSearchPath("headers"),
.unsafeFlags(["-O2"], .when(configuration: .release)),
],
// DEBUG/NDEBUG match the prebuilt React.framework's config-gated C++
// ABI: NDEBUG (Release) strips DebugStringConvertible's vtable and
// shifts the ShadowNode layout that codegen'd Props inherit, so this
Expand All @@ -65,6 +70,7 @@ let package = Package(
// frameworks" warning here. Libraries are held to the API in
// their own packages.
.define("RN_ALLOW_FRAMEWORKS"),
.unsafeFlags(["-O2"], .when(configuration: .release)),
],
linkerSettings: [
.linkedFramework("Foundation")
Expand All @@ -76,7 +82,11 @@ let package = Package(
path: "ReactAppDependencyProvider",
exclude: ["ReactAppDependencyProvider.podspec"],
publicHeadersPath: ".",
cSettings: [.headerSearchPath(".."), .headerSearchPath("headers")],
cSettings: [
.headerSearchPath(".."),
.headerSearchPath("headers"),
.unsafeFlags(["-O2"], .when(configuration: .release)),
],
cxxSettings: [
.headerSearchPath(".."),
.headerSearchPath("headers"),
Expand All @@ -85,6 +95,7 @@ let package = Package(
.define("NDEBUG", .when(configuration: .release)),
// Generated code (see ReactCodegen above).
.define("RN_ALLOW_FRAMEWORKS"),
.unsafeFlags(["-O2"], .when(configuration: .release)),
],
linkerSettings: [
.linkedFramework("Foundation")
Expand Down
41 changes: 41 additions & 0 deletions packages/react-native/scripts/ios-prebuild/__tests__/build-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* 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.
*
* @format
* @noflow
*/

'use strict';

const {getSwiftPackageBuildCommand} = require('../build');

describe('getSwiftPackageBuildCommand', () => {
it('builds Release at -O2', () => {
const command = getSwiftPackageBuildCommand('Release', 'iOS', 'output');

expect(command).toContain('GCC_OPTIMIZATION_LEVEL=2');
});

it('leaves the Debug optimization level to the project', () => {
const command = getSwiftPackageBuildCommand('Debug', 'iOS', 'output');

expect(command).not.toContain('GCC_OPTIMIZATION_LEVEL');
});

it.each(['Debug', 'Release'])('keeps the base %s command', buildType => {
const command = getSwiftPackageBuildCommand(
buildType,
'iOS Simulator',
'output',
);

expect(command).toContain(
'xcodebuild -scheme React -destination "generic/platform=iOS Simulator" -derivedDataPath "output" ' +
`-configuration "${buildType}" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES OTHER_SWIFT_FLAGS="-no-verify-emitted-module-interface" ` +
'DEBUG_INFORMATION_FORMAT="dwarf-with-dsym"',
);
});
});
27 changes: 23 additions & 4 deletions packages/react-native/scripts/ios-prebuild/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,35 @@ function computeFrameworkPaths(
return frameworkPaths;
}

function getSwiftPackageBuildCommand(
buildType /*: BuildFlavor */,
platform /*: Destination */,
outputFolder /*: string */,
) /*: string */ {
const command =
`xcodebuild -scheme React -destination "generic/platform=${platform}" -derivedDataPath "${outputFolder}" ` +
`-configuration "${buildType}" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES OTHER_SWIFT_FLAGS="-no-verify-emitted-module-interface" ` +
`DEBUG_INFORMATION_FORMAT="dwarf-with-dsym"`;
// Xcode defaults Release to -Os. Android builds React Native at -O2 in
// Release. A command-line setting applies to every configuration, so add it
// only to Release builds.
return buildType === 'Release'
? `${command} GCC_OPTIMIZATION_LEVEL=2`
: command;
}

function buildSwiftPackage(
rootFolder /*: string */,
buildFolder /*: string */,
buildType /*: BuildFlavor */,
platform /*: Destination */,
outputFolder /*: string */,
) {
const buildCommand =
`xcodebuild -scheme React -destination "generic/platform=${platform}" -derivedDataPath "${outputFolder}" ` +
`-configuration "${buildType}" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES OTHER_SWIFT_FLAGS="-no-verify-emitted-module-interface" ` +
`DEBUG_INFORMATION_FORMAT="dwarf-with-dsym"`;
const buildCommand = getSwiftPackageBuildCommand(
buildType,
platform,
outputFolder,
);
buildLog(`Building Swift package for ${buildType}`);
buildLog(buildCommand);

Expand Down Expand Up @@ -94,4 +112,5 @@ module.exports = {
buildSwiftPackage,
computeFrameworkPaths,
computeProductsFolder,
getSwiftPackageBuildCommand,
};
1 change: 1 addition & 0 deletions packages/react-native/scripts/react_native_pods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@ def react_native_post_install(
ReactNativePodsUtils.updateOSDeploymentTarget(installer)
ReactNativePodsUtils.set_dynamic_frameworks_flags(installer)
ReactNativePodsUtils.add_ndebug_flag_to_pods_in_release(installer)
ReactNativePodsUtils.set_optimization_level_for_react_native_pods(installer, react_native_path)

if !ReactNativeCoreUtils.build_rncore_from_source()
# The Xcode-26 SWIFT_ENABLE_EXPLICIT_MODULES=NO workaround (#53457) is removed:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* 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.
*
* @format
* @noflow
*/

'use strict';

const fs = require('node:fs');
const path = require('node:path');

const TEMPLATE = fs.readFileSync(
path.resolve(
__dirname,
'..',
'..',
'codegen',
'templates',
'Package.swift.spm-template',
),
'utf8',
);

const RELEASE_O2 = '.unsafeFlags(["-O2"], .when(configuration: .release))';

// Returns the text between the bracket at `openIndex` and its matching close.
function balancedBody(source, openIndex) {
const open = source[openIndex];
const close = {'(': ')', '[': ']'}[open];
let depth = 0;
for (let i = openIndex; i < source.length; i++) {
if (source[i] === open) {
depth++;
} else if (source[i] === close && --depth === 0) {
return source.slice(openIndex + 1, i);
}
}
throw new Error(`Unbalanced ${open} at index ${openIndex}`);
}

function targetBlock(name) {
const re = /\.target\(/g;
let match;
while ((match = re.exec(TEMPLATE)) != null) {
const body = balancedBody(TEMPLATE, match.index + '.target'.length);
if (body.includes(`name: "${name}"`)) {
return body;
}
}
throw new Error(`Target ${name} not found in the codegen template`);
}

function settings(block, key) {
const start = block.indexOf(`${key}: [`);
if (start === -1) {
return null;
}
return balancedBody(block, start + `${key}: `.length);
}

describe('codegen Package.swift.spm-template optimization level', () => {
describe.each(['ReactCodegen', 'ReactAppDependencyProvider'])(
'%s',
targetName => {
it.each(['cSettings', 'cxxSettings'])(
'compiles at -O2 in Release via %s',
key => {
expect(settings(targetBlock(targetName), key)).toContain(RELEASE_O2);
},
);
},
);

it('leaves the headers-only ReactAppHeaders target without flags', () => {
expect(targetBlock('ReactAppHeaders')).not.toContain('unsafeFlags');
});
});
55 changes: 55 additions & 0 deletions scripts/releases/ios-prebuild/__tests__/build-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* 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.
*
* @format
* @noflow
*/

'use strict';

const {getPlatformBuildCommand} = require('../build');

describe('getPlatformBuildCommand', () => {
it('builds Release at -O2', () => {
const command = getPlatformBuildCommand(
'ReactNativeDependencies',
'Release',
'iOS',
'build',
);

expect(command).toContain('GCC_OPTIMIZATION_LEVEL=2');
});

it('leaves the Debug optimization level to the project', () => {
const command = getPlatformBuildCommand(
'ReactNativeDependencies',
'Debug',
'iOS',
'build',
);

expect(command).not.toContain('GCC_OPTIMIZATION_LEVEL');
});

it.each(['Debug', 'Release'])('keeps the base %s command', configuration => {
const command = getPlatformBuildCommand(
'ReactNativeDependencies',
configuration,
'iOS Simulator',
'build',
);

expect(command).toMatch(
new RegExp(
'^xcodebuild -scheme "ReactNativeDependencies" -destination "generic/platform=iOS Simulator" ' +
'-derivedDataPath "build" ' +
`-configuration "${configuration}" ` +
'SKIP_INSTALL=NO\\s+BUILD_LIBRARY_FOR_DISTRIBUTION=YES\\s+DEBUG_INFORMATION_FORMAT=dwarf-with-dsym',
),
);
});
});
Loading
Loading