Skip to content

Commit

Permalink
download hermes-engine when the configuration change (#37850)
Browse files Browse the repository at this point in the history
Summary:
Currently, we ask users to reinstall the pods using the `PRODUCTION` flag when they want to either profile their app or prepare a release.

This way of working with the Release mode is not standard. One of the reason why we introduced it was to provide a different binary for Hermes and reinstalling the pods was the quickest way.

With this change, we are deferring the decision on when Hermes should be installed for apps to the moment where the app is actually build by the system.

These changes are not applied to Nightlies, when a specific tarball is passed to the cocoapods using the `HERMES_ENGINE_TARBALL_PATH` env var, and when hermes is built from source as in these scenarios we are usually not interested in building for Release.

The system is also smart enough not to redownload the tarball if the configuration does not change. It assumes that the default configuration when the pods are installed for the first time is Debug.

## Changelog:

[IOS] [CHANGED] - Download the right `hermes-engine` configuration at build time.

Pull Request resolved: #37850

Test Plan:
- CircleCI green for the Release template jobs
- Tested locally modifying the `hermes-utils` to force specific versions.
- Tested locally with RNTestProject

Reviewed By: dmytrorykun

Differential Revision: D46687390

Pulled By: cipolleschi

fbshipit-source-id: 375406e0ab351a5d1f5d5146e724f5ed0cd77949
  • Loading branch information
Riccardo Cipolleschi authored and facebook-github-bot committed Jun 14, 2023
1 parent b3cc19c commit 332be0f
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 71 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ DerivedData
*.xcuserstate
project.xcworkspace
**/.xcode.env.local
/poackages/react-native/sdks/downloads/

# Gradle
/build/
Expand Down
23 changes: 15 additions & 8 deletions packages/react-native/scripts/cocoapods/__tests__/utils-test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,21 +178,26 @@ def test_hasPod_whenInstallerHasPod_returnTrue
# Test - Set GCC Preprocessor Definition for React-hermes #
# ======================================================== #

def test_SetGCCPreprocessorDefinitionForReactHermes_itSetsThePreprocessorForDebug
def test_SetGCCPreprocessorDefinitionForHermes_itSetsThePreprocessorForDebug
# Arrange
react_hermes_name = "React-hermes"
react_core_name = "React-Core"
react_hermes_debug_config = BuildConfigurationMock.new("Debug")
react_hermes_release_config = BuildConfigurationMock.new("Release")
react_core_debug_config = BuildConfigurationMock.new("Debug")
react_core_release_config = BuildConfigurationMock.new("Release")
react_hermes_target = TargetMock.new(react_hermes_name, [react_hermes_debug_config, react_hermes_release_config])
react_core_target = TargetMock.new(react_core_name, [react_core_debug_config, react_core_release_config])
hermes_engine_name = "hermes-engine"
react_hermes_debug_config = BuildConfigurationMock.new("Debug")
react_hermes_release_config = BuildConfigurationMock.new("Release")
react_core_debug_config = BuildConfigurationMock.new("Debug")
react_core_release_config = BuildConfigurationMock.new("Release")
hermes_engine_debug_config = BuildConfigurationMock.new("Debug")
hermes_engine_release_config = BuildConfigurationMock.new("Release")
react_hermes_target = TargetMock.new(react_hermes_name, [react_hermes_debug_config, react_hermes_release_config])
react_core_target = TargetMock.new(react_core_name, [react_core_debug_config, react_core_release_config])
hermes_engine_target = TargetMock.new(hermes_engine_name, [hermes_engine_debug_config, hermes_engine_release_config])

installer = InstallerMock.new(
:pod_target_installation_results => {
react_hermes_name => TargetInstallationResultMock.new(react_hermes_target, react_hermes_target),
react_core_name => TargetInstallationResultMock.new(react_core_target, react_core_target),
hermes_engine_name => TargetInstallationResultMock.new(hermes_engine_target, hermes_engine_target),
}
)

Expand All @@ -201,11 +206,13 @@ def test_SetGCCPreprocessorDefinitionForReactHermes_itSetsThePreprocessorForDebu

# Assert
build_setting = "GCC_PREPROCESSOR_DEFINITIONS"
expected_value = "HERMES_ENABLE_DEBUGGER=1"
expected_value = "$(inherited) HERMES_ENABLE_DEBUGGER=1"
assert_equal(expected_value, react_hermes_debug_config.build_settings[build_setting])
assert_nil(react_hermes_release_config.build_settings[build_setting])
assert_nil(react_core_debug_config.build_settings[build_setting])
assert_nil(react_core_release_config.build_settings[build_setting])
assert_equal(expected_value, hermes_engine_debug_config.build_settings[build_setting])
assert_nil(hermes_engine_release_config.build_settings[build_setting])
end

# ============================ #
Expand Down
4 changes: 3 additions & 1 deletion packages/react-native/scripts/cocoapods/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def self.has_pod(installer, name)

def self.set_gcc_preprocessor_definition_for_React_hermes(installer)
self.add_build_settings_to_pod(installer, "GCC_PREPROCESSOR_DEFINITIONS", "HERMES_ENABLE_DEBUGGER=1", "React-hermes", "Debug")
self.add_build_settings_to_pod(installer, "GCC_PREPROCESSOR_DEFINITIONS", "HERMES_ENABLE_DEBUGGER=1", "hermes-engine", "Debug")
end

def self.turn_off_resource_bundle_react_core(installer)
Expand Down Expand Up @@ -153,7 +154,8 @@ def self.add_build_settings_to_pod(installer, settings_name, settings_value, tar
if pod_name.to_s == target_pod_name
target_installation_result.native_target.build_configurations.each do |config|
if configuration == nil || (configuration != nil && configuration == config.name)
config.build_settings[settings_name] = settings_value
config.build_settings[settings_name] ||= '$(inherited) '
config.build_settings[settings_name] << settings_value
end
end
end
Expand Down
31 changes: 23 additions & 8 deletions packages/react-native/sdks/hermes-engine/hermes-engine.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ require_relative "./hermes-utils.rb"

react_native_path = File.join(__dir__, "..", "..")

# Whether Hermes is built for Release or Debug is determined by the PRODUCTION envvar.
build_type = ENV['PRODUCTION'] == "1" ? :release : :debug

# package.json
package = JSON.parse(File.read(File.join(react_native_path, "package.json")))
version = package['version']
Expand All @@ -23,7 +20,7 @@ git = "https://github.com/facebook/hermes.git"

abort_if_invalid_tarball_provided!

source = compute_hermes_source(build_from_source, hermestag_file, git, version, build_type, react_native_path)
source = compute_hermes_source(build_from_source, hermestag_file, git, version, react_native_path)

Pod::Spec.new do |spec|
spec.name = "hermes-engine"
Expand All @@ -42,22 +39,40 @@ Pod::Spec.new do |spec|
spec.pod_target_xcconfig = {
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17",
"CLANG_CXX_LIBRARY" => "compiler-default"
}.merge!(build_type == :debug ? { "GCC_PREPROCESSOR_DEFINITIONS" => "HERMES_ENABLE_DEBUGGER=1" } : {})
}

spec.ios.vendored_frameworks = "destroot/Library/Frameworks/ios/hermes.framework"
spec.osx.vendored_frameworks = "destroot/Library/Frameworks/macosx/hermes.framework"

if source[:http] then

spec.subspec 'Pre-built' do |ss|
ss.preserve_paths = ["destroot/bin/*"].concat(build_type == :debug ? ["**/*.{h,c,cpp}"] : [])
ss.preserve_paths = ["destroot/bin/*"].concat(["**/*.{h,c,cpp}"])
ss.source_files = "destroot/include/**/*.h"
ss.exclude_files = ["destroot/include/jsi/jsi/JSIDynamic.{h,cpp}", "destroot/include/jsi/jsi/jsilib-*.{h,cpp}"]
ss.header_mappings_dir = "destroot/include"
ss.ios.vendored_frameworks = "destroot/Library/Frameworks/universal/hermes.xcframework"
ss.osx.vendored_frameworks = "destroot/Library/Frameworks/macosx/hermes.framework"
end


# Right now, even reinstalling pods with the PRODUCTION flag turned on, does not change the version of hermes that is downloaded
# To remove the PRODUCTION flag, we want to download the right version of hermes on the flight
# we do so in a pre-build script we invoke from the Xcode build pipeline
# We use this only for Apps created using the template. RNTester and Nightlies should not be used to build for Release.
# We ignore this if we provide a specific tarball: the assumption here is that if you are providing a tarball, is because you want to
# test something specific for that tarball.
if source[:http].include?('https://repo1.maven.org/')
spec.script_phase = {
:name => "[Hermes] Replace Hermes for the right configuration, if needed",
:execution_position => :before_compile,
:script => <<-EOS
. "$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh"
"$NODE_BINARY" "$REACT_NATIVE_PATH/sdks/hermes-engine/utils/replace_hermes_version.js" -c "$CONFIGURATION" -r "#{version}" -p "$REACT_NATIVE_PATH"
EOS
}
end

elsif source[:git] then

spec.subspec 'Hermes' do |ss|
Expand Down Expand Up @@ -96,15 +111,15 @@ Pod::Spec.new do |spec|
{
:name => '[RN] [1] Build Hermesc',
:script => <<-EOS
. ${PODS_ROOT}/../.xcode.env
. "${REACT_NATIVE_PATH}/scripts/xcode/with-environment.sh"
export CMAKE_BINARY=${CMAKE_BINARY:-#{CMAKE_BINARY}}
. ${REACT_NATIVE_PATH}/sdks/hermes-engine/utils/build-hermesc-xcode.sh #{hermesc_path}
EOS
},
{
:name => '[RN] [2] Build Hermes',
:script => <<-EOS
. ${PODS_ROOT}/../.xcode.env
. "${REACT_NATIVE_PATH}/scripts/xcode/with-environment.sh"
export CMAKE_BINARY=${CMAKE_BINARY:-#{CMAKE_BINARY}}
. ${REACT_NATIVE_PATH}/sdks/hermes-engine/utils/build-hermes-xcode.sh #{version} #{hermesc_path}/ImportHermesc.cmake
EOS
Expand Down
38 changes: 25 additions & 13 deletions packages/react-native/sdks/hermes-engine/hermes-utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def abort_if_invalid_tarball_provided!()
# - react_native_path: path to react native
#
# Returns: a properly configured source object
def compute_hermes_source(build_from_source, hermestag_file, git, version, build_type, react_native_path)
def compute_hermes_source(build_from_source, hermestag_file, git, version, react_native_path)
source = {}

if ENV.has_key?('HERMES_ENGINE_TARBALL_PATH')
Expand All @@ -43,8 +43,10 @@ def compute_hermes_source(build_from_source, hermestag_file, git, version, build
else
build_hermes_from_source(source, git)
end
elsif hermes_artifact_exists(release_tarball_url(version, build_type))
use_release_tarball(source, version, build_type)
elsif hermes_artifact_exists(release_tarball_url(version, :debug))
use_release_tarball(source, version, :debug)
download_stable_hermes(react_native_path, version, :debug)
download_stable_hermes(react_native_path, version, :release)
elsif hermes_artifact_exists(nightly_tarball_url(version).gsub("\\", ""))
use_nightly_tarball(source, react_native_path, version)
else
Expand Down Expand Up @@ -100,6 +102,25 @@ def putsIfPodPresent(message, level = 'warning')
end
end

def download_stable_hermes(react_native_path, version, configuration)
tarball_url = release_tarball_url(version, configuration)
download_hermes_tarball(react_native_path, tarball_url, version, configuration)
end

def download_hermes_tarball(react_native_path, tarball_url, version, configuration)
destination_folder = "#{react_native_path}/sdks/downloads"
destination_path = configuration == nil ?
"#{destination_folder}/hermes-ios-#{version}.tar.gz" :
"#{destination_folder}/hermes-ios-#{version}-#{configuration}.tar.gz"

unless File.exist?(destination_path)
# Download to a temporary file first so we don't cache incomplete downloads.
tmp_file = "#{destination_folder}/hermes-ios.download"
`mkdir -p "#{destination_folder}" && curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"`
end
return destination_path
end

# This function downloads the nightly prebuilt version of Hermes based on the passed version
# and save it in the node_module/react_native/sdks/downloads folder
# It then returns the path to the hermes tarball
Expand All @@ -110,16 +131,7 @@ def putsIfPodPresent(message, level = 'warning')
# Returns: the path to the downloaded Hermes tarball
def download_nightly_hermes(react_native_path, version)
tarball_url = nightly_tarball_url(version)

destination_folder = "#{react_native_path}/sdks/downloads"
destination_path = "#{destination_folder}/hermes-ios-#{version}.tar.gz"

unless File.exist?(destination_path)
# Download to a temporary file first so we don't cache incomplete downloads.
tmp_file = "#{destination_folder}/hermes-ios.download"
`mkdir -p "#{destination_folder}" && curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"`
end
return destination_path
return download_stable_hermes(react_native_path, tarball_url, version, nil)
end

def nightly_tarball_url(version)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* 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
*/

'use strict';

const yargs = require('yargs');
const fs = require('fs');
const {execSync} = require('child_process');

const LAST_BUILD_FILENAME = '.last_build_configuration';

function validateBuildConfiguration(configuration) {
if (!['Debug', 'Release'].includes(configuration)) {
throw new Error(`Invalid configuration ${configuration}`);
}
}

function validateVersion(version) {
if (version == null || version === '') {
throw new Error('Version cannot be empty');
}
}

function shouldReplaceHermesConfiguration(configuration) {
const fileExists = fs.existsSync(LAST_BUILD_FILENAME);

if (fileExists) {
console.log(`Found ${LAST_BUILD_FILENAME} file`);
const oldConfiguration = fs.readFileSync(LAST_BUILD_FILENAME).toString();
if (oldConfiguration === configuration) {
console.log('No need to download a new build of Hermes!');
return false;
}
}

// Assumption: if there is no stored last build, we assume that it was build for debug.
if (!fs.existsSync && configuration === 'Debug') {
console.log(
'File does not exists, but Debug configuration. No need to download a new build of Hermes!',
);
return false;
}

return true;
}

function replaceHermesConfiguration(configuration, version, reactNativePath) {
const tarballURLPath = `${reactNativePath}/sdks/downloads/hermes-ios-${version}-${configuration}.tar.gz`;

const finalLocation = 'hermes-engine';
console.log('Preparing the final location');
fs.rmSync(finalLocation, {force: true, recursive: true});
fs.mkdirSync(finalLocation, {recursive: true});

console.log('Extracting the tarball');
execSync(`tar -xf ${tarballURLPath} -C ${finalLocation}`);
}

function updateLastBuildConfiguration(configuration) {
fs.writeFileSync(LAST_BUILD_FILENAME, configuration);
}

function main(configuration, version, reactNativePath) {
validateBuildConfiguration(configuration);
validateVersion(version);

if (!shouldReplaceHermesConfiguration(configuration)) {
return;
}

replaceHermesConfiguration(configuration, version, reactNativePath);
updateLastBuildConfiguration(configuration);
console.log('Done replacing hermes-engine');
}

// This script is executed in the Pods folder, which is usually not synched to Github, so it should be ok
const argv = yargs
.option('c', {
alias: 'configuration',
description:
'Configuration to use to download the right Hermes version. Allowed values are "Debug" and "Release".',
})
.option('r', {
alias: 'reactNativeVersion',
description:
'The Version of React Native associated with the Hermes tarball.',
})
.option('p', {
alias: 'reactNativePath',
description: 'The path to the React Native root folder',
})
.usage('Usage: $0 -c Debug -r <version> -p <path/to/react-native>').argv;

const configuration = argv.configuration;
const version = argv.reactNativeVersion;
const reactNativePath = argv.reactNativePath;

main(configuration, version, reactNativePath);
Loading

0 comments on commit 332be0f

Please sign in to comment.