first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const minKotlinVersion = '2.0.0';
const maxKotlinVersion = '2.3.10';
const groupId = 'com.google.devtools.ksp';
const artifactId = 'symbol-processing-gradle-plugin';
const path = require('path').resolve(
__dirname,
'../android/expo-gradle-plugin/expo-autolinking-plugin/src/main/kotlin/expo/modules/plugin/KSPLookup.kt'
);
const numberPerPage = 30;
const githubApiPath = '/repos/google/ksp/releases';
async function* fetchKSPReleases() {
let currentPage = 1;
while (true) {
const apiPath = `${githubApiPath}?per_page=${numberPerPage}&page=${currentPage}`;
console.log(`Fetching versions from: ${apiPath}...`);
const output = execSync(`gh api "${apiPath}"`, { encoding: 'utf-8' });
const data = JSON.parse(output);
const versions = data
.map((release) => release.tag_name)
// Filter out release candidates, beta and milestone versions
.filter((version) => !/(rc|beta|m1)/i.test(version));
yield versions;
currentPage += 1;
}
}
async function filterByKotinVersion(generator, minKotlinVersion, maxKotlinVersion) {
let hasMoreData = true;
const result = [];
while (hasMoreData) {
const { value } = await generator.next();
const versions = value.map((version) => {
const [kotlinVersion] = version.split('-');
return { kotlinVersion, version };
});
// If the version is lower than the minKotlinVersion, we can stop fetching more data
hasMoreData = !versions.some(({ kotlinVersion }) => kotlinVersion < minKotlinVersion);
result.push(
...versions.filter(
({ kotlinVersion }) =>
kotlinVersion >= minKotlinVersion && kotlinVersion <= maxKotlinVersion
)
);
}
return result;
}
// Group versions by kotlinVersion and return the most recent version for each kotlinVersion
function groupByKotlinVersion(versions) {
const mostRecentVersions = {};
// Fill the mostRecentVersions object with the newest version of each kotlinVersion
versions.forEach(({ kotlinVersion, version }) => {
if (!mostRecentVersions[kotlinVersion] || mostRecentVersions[kotlinVersion] < version) {
mostRecentVersions[kotlinVersion] = version;
}
});
// Convert the results object back into an array
return Object.entries(mostRecentVersions).map(([kotlinVersion, version]) => ({
kotlinVersion,
version,
}));
}
async function generateKSPLookup() {
const versionsGenerator = fetchKSPReleases(groupId, artifactId, minKotlinVersion);
const versions = groupByKotlinVersion(
await filterByKotinVersion(versionsGenerator, minKotlinVersion, maxKotlinVersion)
);
console.log(`Found stable versions for ${groupId}:${artifactId}:`);
for (const { kotlinVersion, version } of versions) {
console.log(` ${kotlinVersion} => ${version}`);
}
console.log('Genereating KSP lookup file...');
const fileContent = `// Copyright 2015-present 650 Industries. All rights reserved.
// Generated using './scripts/generateKSPLookUp.js'
package expo.modules.plugin
val KSPLookup = mapOf(
${versions.map(({ kotlinVersion, version }) => ` "${kotlinVersion}" to "${version}"`).join(',\n')}
)
`;
fs.writeFileSync(path, fileContent, 'utf-8');
}
generateKSPLookup();

View File

@@ -0,0 +1,281 @@
require_relative 'constants'
require_relative 'package'
require_relative 'packages_config'
# Require extensions to CocoaPods' classes
require_relative 'cocoapods/sandbox'
require_relative 'cocoapods/target_definition'
require_relative 'cocoapods/user_project_integrator'
require_relative 'cocoapods/installer'
module Expo
class AutolinkingManager
require 'colored2'
include Pod
public def initialize(podfile, target_definition, options)
@podfile = podfile
@target_definition = target_definition
@options = options
validate_target_definition()
resolve_result = resolve()
Expo::PackagesConfig.instance.coreFeatures = resolve_result['coreFeatures']
@packages = resolve_result['modules'].map { |json_package| Package.new(json_package) }
@extraPods = resolve_result['extraDependencies']
end
public def use_expo_modules!
if has_packages?
return
end
global_flags = @options.fetch(:flags, {})
tests_only = @options.fetch(:testsOnly, false)
include_tests = @options.fetch(:includeTests, false)
# Add any additional framework modules to patch using the patched Podfile class in installer.rb
# We'll be reading from Podfile.properties.json and optionally parameters passed to use_expo_modules!
podfile_properties = JSON.parse(File.read(File.join(Pod::Config.instance.project_root, 'Podfile.properties.json'))) rescue {}
additional_framework_modules_to_patch = @options.fetch(:additionalFrameworkModulesToPatch, []) +
JSON.parse(podfile_properties['ios.forceStaticLinking'] || "[]")
Pod::UI.puts("Forcing static linking for pods: #{additional_framework_modules_to_patch}") if !additional_framework_modules_to_patch.empty?
@podfile.expo_add_modules_to_patch(additional_framework_modules_to_patch) if !additional_framework_modules_to_patch.empty?
project_directory = Pod::Config.instance.project_root
UI.section 'Using Expo modules' do
@packages.each { |package|
package.pods.each { |pod|
# The module can already be added to the target, in which case we can just skip it.
# This allows us to add a pod before `use_expo_modules` to provide custom flags.
if @target_definition.dependencies.any? { |dependency| dependency.name == pod.pod_name }
UI.message '— ' << package.name.green << ' is already added to the target'.yellow
next
end
# Skip if the podspec doesn't include the platform for the current target.
unless pod.supports_platform?(@target_definition.platform)
UI.message '- ' << package.name.green << " doesn't support #{@target_definition.platform.string_name} platform".yellow
next
end
# Ensure that the dependencies of packages with Swift code use modular headers, otherwise
# `pod install` may fail if there is no `use_modular_headers!` declaration or
# `:modular_headers => true` is not used for this particular dependency.
# The latter require adding transitive dependencies to user's Podfile that we'd rather like to avoid.
if package.has_something_to_link?
use_modular_headers_for_dependencies(pod.spec.all_dependencies)
end
podspec_dir_path = Pathname.new(pod.podspec_dir).relative_path_from(project_directory).to_path
debug_configurations = @target_definition.build_configurations ? @target_definition.build_configurations.select { |config| config.include?('Debug') }.keys : ['Debug']
pod_options = {
:path => podspec_dir_path,
:configuration => package.debugOnly ? debug_configurations : [] # An empty array means all configurations
}.merge(global_flags, package.flags)
if tests_only || include_tests
test_specs_names = pod.spec.test_specs.map { |test_spec|
test_spec.name.delete_prefix(pod.spec.name + "/")
}
# Jump to the next package when it doesn't have any test specs (except interfaces, they're required)
# TODO: Can remove interface check once we move all the interfaces into the core.
next if tests_only && test_specs_names.empty? && !pod.pod_name.end_with?('Interface')
pod_options[:testspecs] = test_specs_names
end
# Install the pod.
@podfile.pod(pod.pod_name, pod_options)
# TODO: Can remove this once we move all the interfaces into the core.
next if pod.pod_name.end_with?('Interface')
UI.message "#{package.name.green} (#{package.version})"
}
}
end
@extraPods.each { |pod|
UI.info "Adding extra pod - #{pod['name']} (#{pod['version'] || '*'})"
requirements = Array.new
requirements << pod['version'] if pod['version']
options = Hash.new
options[:configurations] = pod['configurations'] if pod['configurations']
options[:modular_headers] = pod['modular_headers'] if pod['modular_headers']
options[:source] = pod['source'] if pod['source']
options[:path] = pod['path'] if pod['path']
options[:podspec] = pod['podspec'] if pod['podspec']
options[:testspecs] = pod['testspecs'] if pod['testspecs']
options[:git] = pod['git'] if pod['git']
options[:branch] = pod['branch'] if pod['branch']
options[:tag] = pod['tag'] if pod['tag']
options[:commit] = pod['commit'] if pod['commit']
requirements << options
@podfile.pod(pod['name'], *requirements)
}
self
end
# Spawns `expo-module-autolinking generate-modules-provider` command.
public def generate_modules_provider(target_name, target_path)
Process.wait IO.popen(generate_modules_provider_command_args(target_path)).pid
end
# If there is any package to autolink.
public def has_packages?
@packages.empty?
end
# Filters only these packages that needs to be included in the generated modules provider.
public def packages_to_generate
platform = @target_definition.platform
@packages.select do |package|
# Check whether the package has any module to autolink
# and if there is any pod that supports target's platform.
package.has_something_to_link? && package.pods.any? { |pod| pod.supports_platform?(platform) }
end
end
# Returns the provider name which is also a name of the generated file
public def modules_provider_name
@options.fetch(:providerName, Constants::MODULES_PROVIDER_FILE_NAME)
end
# Absolute path to `Pods/Target Support Files/<pods target name>/<modules provider file>` within the project path
public def modules_provider_path(target)
File.join(target.support_files_dir, modules_provider_name)
end
# For now there is no need to generate the modules provider for testing.
public def should_generate_modules_provider?
return !@options.fetch(:testsOnly, false)
end
# Returns the platform name of the current target definition.
# Note that it is suitable to be presented to the user (i.e. is not lowercased).
public def platform_name
return @target_definition.platform&.string_name
end
# Returns the app project root if provided in the options.
public def custom_app_root
# TODO: Follow up on renaming `:projectRoot` and migrate to `appRoot`
return @options.fetch(:appRoot, @options.fetch(:projectRoot, nil))
end
public def resolve
json = []
IO.popen(resolve_command_args) do |data|
while line = data.gets
json << line
end
end
begin
JSON.parse(json.join())
rescue => error
raise "Couldn't parse JSON coming from `expo-modules-autolinking` command:\n#{error}"
end
end
public def base_command_args
search_paths = @options.fetch(:searchPaths, @options.fetch(:modules_paths, nil))
ignore_paths = @options.fetch(:ignorePaths, nil)
exclude = @options.fetch(:exclude, [])
args = []
if !search_paths.nil? && !search_paths.empty?
args.concat(search_paths)
end
if !ignore_paths.nil? && !ignore_paths.empty?
args.concat(['--ignore-paths'], ignore_paths)
end
if !exclude.nil? && !exclude.empty?
args.concat(['--exclude'], exclude)
end
args
end
private def node_command_args(command_name)
eval_command_args = [
'node',
'--no-warnings',
'--eval',
'require(\'expo/bin/autolinking\')',
'expo-modules-autolinking',
command_name,
'--platform',
'apple'
]
return eval_command_args.concat(base_command_args())
end
private def resolve_command_args
resolve_command_args = ['--json']
project_root = @options.fetch(:projectRoot, nil)
if project_root
resolve_command_args.concat(['--project-root', project_root])
end
node_command_args('resolve').concat(resolve_command_args)
end
public def generate_modules_provider_command_args(target_path)
command_args = ['--target', target_path]
if !custom_app_root.nil?
command_args.concat(['--app-root', custom_app_root])
end
node_command_args('generate-modules-provider').concat(
command_args,
['--packages'],
packages_to_generate.map(&:name)
)
end
private def use_modular_headers_for_dependencies(dependencies)
dependencies.each { |dependency|
# The dependency name might be a subspec like `ReactCommon/turbomodule/core`,
# but the modular headers need to be enabled for the entire `ReactCommon` spec anyway,
# so we're stripping the subspec path from the dependency name.
root_spec_name = dependency.name.partition('/').first
unless @target_definition.build_pod_as_module?(root_spec_name)
UI.info "[Expo] ".blue << "Enabling modular headers for pod #{root_spec_name.green}"
# This is an equivalent to setting `:modular_headers => true` for the specific dependency.
@target_definition.set_use_modular_headers_for_pod(root_spec_name, true)
end
}
end
# Validates whether the Expo modules can be autolinked in the given target definition.
private def validate_target_definition
# The platform must be declared within the current target (e.g. `platform :ios, '13.0'`)
if platform_name.nil?
raise "Undefined platform for target #{@target_definition.name}, make sure to call `platform` method globally or inside the target"
end
# The declared platform must be iOS, macOS or tvOS, others are not supported.
unless ['iOS', 'macOS', 'tvOS'].include?(platform_name)
raise "Target #{@target_definition.name} is dedicated to #{platform_name} platform, which is not supported by Expo Modules"
end
end
end # class AutolinkingManager
end # module Expo

View File

@@ -0,0 +1,152 @@
# Overrides CocoaPods `Installer`/'Podfile' classes to patch podspecs on the fly
# See: https://github.com/CocoaPods/CocoaPods/blob/master/lib/cocoapods/installer.rb
# See: https://github.com/CocoaPods/Core/blob/master/lib/cocoapods-core/podfile.rb#L160
#
# This is necessary to disable `USE_FRAMEWORKS` for specific pods that include
# React Native Core headers in their public headers, which causes issues when
# building them as dynamic frameworks with modular headers enabled.
module Pod
class Podfile
public
def framework_modules_to_patch
@framework_modules_to_patch ||= ['ExpoModulesCore', 'ExpoModulesJSI', 'Expo', 'ReactAppDependencyProvider', 'expo-dev-menu']
end
def expo_add_modules_to_patch(modules)
framework_modules_to_patch.concat(modules)
end
end
class Installer
private
_original_perform_post_install_actions = instance_method(:perform_post_install_actions)
_original_run_podfile_pre_install_hooks = instance_method(:run_podfile_pre_install_hooks)
_script_phase_name = '[Expo Autolinking] Run Codegen with autolinking'
public
define_method(:perform_post_install_actions) do
# Call original implementation first
_original_perform_post_install_actions.bind(self).()
# Next we'll perform an Expo workaround for Codegen in React Native where it uses the wrong output path for
# the generated files. This can be remove when the following PR is merged and released upstream:
# https://github.com/facebook/react-native/pull/54066
# TODO: chrfalch - remove when RN PR is released
# Find the ReactCodegen pod target in the pods project
react_codegen_native_target = self.pods_project.targets.find { |target| target.name == 'ReactCodegen' }
if react_codegen_native_target
# Check if the build phase already exists
already_exists = react_codegen_native_target.build_phases.any? do |phase|
phase.is_a?(Xcodeproj::Project::Object::PBXShellScriptBuildPhase) && phase.name == _script_phase_name
end
if !already_exists
Pod::UI.puts "[Expo] ".blue + "Adding '#{_script_phase_name}' build phase to ReactCodegen"
# Create the new shell script build phase
phase = react_codegen_native_target.new_shell_script_build_phase(_script_phase_name)
phase.shell_path = '/bin/sh'
phase.shell_script = <<~SH
# Remove this step when the fix is merged and released.
# See: https://github.com/facebook/react-native/pull/54066
# This re-runs Codegen without the broken "scripts/react_native_pods_utils/script_phases.sh" script, causing Codegen to run without autolinking.
# Instead of using "script_phases.sh" which always runs inside DerivedData, we run it in the normal "/ios" folder
# See: https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods_utils/script_phases.sh
pushd "$PODS_ROOT/../" > /dev/null
RCT_SCRIPT_POD_INSTALLATION_ROOT="$PODS_ROOT/.."
popd >/dev/null
export RCT_SCRIPT_RN_DIR="$REACT_NATIVE_PATH" # This is set by Expo
export RCT_SCRIPT_APP_PATH="$RCT_SCRIPT_POD_INSTALLATION_ROOT/.."
export RCT_SCRIPT_OUTPUT_DIR="$RCT_SCRIPT_POD_INSTALLATION_ROOT"
export RCT_SCRIPT_TYPE="withCodegenDiscovery"
# This is the broken script that runs inside DerivedData, meaning it can't find the autolinking result in `ios/build/generated/autolinking.json`.
# Resulting in Codegen running with it's own autolinking, not discovering transitive peer dependencies.
# export SCRIPT_PHASES_SCRIPT="$RCT_SCRIPT_RN_DIR/scripts/react_native_pods_utils/script_phases.sh"
export WITH_ENVIRONMENT="$RCT_SCRIPT_RN_DIR/scripts/xcode/with-environment.sh"
# Start of workaround code
# Load the $NODE_BINARY from the "with-environment.sh" script
source "$WITH_ENVIRONMENT"
# Run this script directly in the right folders:
# https://github.com/facebook/react-native/blob/3f7f9d8fb8beb41408d092870a7c7cac58029a4d/packages/react-native/scripts/react_native_pods_utils/script_phases.sh#L96-L101
pushd "$RCT_SCRIPT_RN_DIR" >/dev/null || exit 1
set -x
"$NODE_BINARY" "scripts/generate-codegen-artifacts.js" --path "$RCT_SCRIPT_APP_PATH" --outputPath "$RCT_SCRIPT_OUTPUT_DIR" --targetPlatform "ios"
set +x
popd >/dev/null || exit 1
# End of workaround code
SH
# Find the index of the "Compile Sources" phase (PBXSourcesBuildPhase)
compile_sources_index = react_codegen_native_target.build_phases.find_index do |p|
p.is_a?(Xcodeproj::Project::Object::PBXSourcesBuildPhase)
end
if compile_sources_index
# Remove the phase from its current position (it was added at the end)
react_codegen_native_target.build_phases.delete(phase)
# Insert it before the "Compile Sources" phase
react_codegen_native_target.build_phases.insert(compile_sources_index, phase)
else
Pod::UI.puts "[Expo] ".yellow + "Could not find 'Compile Sources' phase, build phase added at default position"
end
end
else
Pod::UI.puts "[Expo] ".yellow + "ReactCodegen target not found in pods project"
end
end
define_method(:run_podfile_pre_install_hooks) do
# Call original implementation first
_original_run_podfile_pre_install_hooks.bind(self).()
return unless should_disable_use_frameworks_for_core_expo_pods?()
# Disable USE_FRAMEWORKS in core targets when USE_FRAMEWORKS is set
# This method overrides the build_type field to always use static_library for
# the following pod targets:
# - ExpoModulesCore, Expo, ReactAppDependencyProvider, expo-dev-menu
# These are all including files from React Native Core in their public header files,
# which causes their own modular headers to be invalid.
Pod::UI.puts "[Expo] ".blue + "Disabling USE_FRAMEWORKS for modules #{@podfile.framework_modules_to_patch.join(', ')}"
self.pod_targets.each do |t|
if @podfile.framework_modules_to_patch.include?(t.name)
def t.build_type
Pod::BuildType.static_library
end
end
end
end
private
# We should only disable USE_FRAMEWORKS for specific pods when:
# - RCT_USE_PREBUILT_RNCORE is not '1'
# - USE_FRAMEWORKS is not set
def should_disable_use_frameworks_for_core_expo_pods?()
return false if ENV['RCT_USE_PREBUILT_RNCORE'] != '1'
return true if get_linkage?() != nil
false
end
# Returns the linkage type if USE_FRAMEWORKS is set, otherwise returns nil
def get_linkage?()
return nil if ENV["USE_FRAMEWORKS"] == nil
return :dynamic if ENV["USE_FRAMEWORKS"].downcase == 'dynamic'
return :static if ENV["USE_FRAMEWORKS"].downcase == 'static'
nil
end
end
end

View File

@@ -0,0 +1,43 @@
# Overrides CocoaPods `Sandbox` class to patch podspecs on the fly
# See: https://github.com/CocoaPods/CocoaPods/blob/master/lib/cocoapods/sandbox.rb
require 'json'
REACT_DEFINE_MODULES_LIST = [
'React-hermes',
'React-jsc',
]
module Pod
class Sandbox
private
_original_store_podspec = instance_method(:store_podspec)
public
define_method(:store_podspec) do |name, podspec, _external_source, json|
spec = _original_store_podspec.bind(self).(name, podspec, _external_source, json)
patched_spec = nil
# Patch podspecs to define module
if REACT_DEFINE_MODULES_LIST.include? name
spec_json = JSON.parse(podspec.to_pretty_json)
spec_json['pod_target_xcconfig'] ||= {}
spec_json['pod_target_xcconfig']['DEFINES_MODULE'] = 'YES'
patched_spec = Specification.from_json(spec_json.to_json)
end
if patched_spec != nil
# Store the patched spec with original checksum and local saved file path
patched_spec.defined_in_file = spec.defined_in_file
patched_spec.instance_variable_set(:@checksum, spec.checksum)
@stored_podspecs[spec.name] = patched_spec
return patched_spec
end
return spec
end # define_method(:store_podspec)
end # class Sandbox
end # module Pod

View File

@@ -0,0 +1,22 @@
# Overrides CocoaPods class as the AutolinkingManager is in fact part of
# the target definitions and we need to refer to it at later steps.
# See: https://github.com/CocoaPods/Core/blob/master/lib/cocoapods-core/podfile/target_definition.rb
module Pod
class Podfile
class TargetDefinition
public
attr_writer :autolinking_manager
def autolinking_manager
if @autolinking_manager.present? || root?
@autolinking_manager
else
parent.autolinking_manager
end
end
end
end
end

View File

@@ -0,0 +1,62 @@
require_relative '../project_integrator'
# Unfortunately there is no good and official place that we could use to generate module providers
# and integrate them with user targets by operating on the PBXProj kept and saved by CocoaPods.
# So we have to hook into the private method called `integrate_user_targets`
# where we have public access to everything that we need.
# Original implementation: https://github.com/CocoaPods/CocoaPods/blob/master/lib/cocoapods/installer/user_project_integrator.rb
module Pod
class Installer
class UserProjectIntegrator
include Expo::ProjectIntegrator
private
_original_integrate_user_targets = instance_method(:integrate_user_targets)
# Integrates the targets of the user projects with the libraries
# generated from the {Podfile}.
#
# @note {TargetDefinition} without dependencies are skipped to prevent
# creating empty libraries for target definitions which are only
# wrappers for others.
#
# @return [void]
#
define_method(:integrate_user_targets) do
# Call original method first
results = _original_integrate_user_targets.bind(self).()
UI.message '- Integrating Expo modules providers' do
# All user targets mapped to user projects.
all_projects = targets.map { |target| target.user_project }.uniq
# Array of projects to integrate is usually a subset of `all_projects`,
# and it might be empty subsequent installs after the first install.
# CocoaPods integrates only these ones.
projects_to_integrate = user_projects_to_integrate()
# However, we need to make sure that all projects are integrated,
# regardless of the CocoaPods cache.
all_projects.each do |project|
project_targets = targets.select { |target| target.user_project.equal?(project) }
Expo::ProjectIntegrator::integrate_targets_in_project(project_targets, project)
Expo::ProjectIntegrator::remove_nils_from_source_files(project)
Expo::ProjectIntegrator::set_autolinking_configuration(project)
# CocoaPods saves the projects to integrate at the next step,
# but in some cases we're modifying other projects as well.
# Below we make sure the project will be saved and no more than once!
if project.dirty? && !projects_to_integrate.include?(project)
save_projects([project])
end
end
end
results
end
end # class UserProjectIntegrator
end # class Installer
end # module Pod

View File

@@ -0,0 +1,9 @@
module Expo
module Constants
GENERATED_GROUP_NAME = 'ExpoModulesProviders'
MODULES_PROVIDER_FILE_NAME = 'ExpoModulesProvider.swift'
CONFIGURE_PROJECT_BUILD_SCRIPT_NAME = '[Expo] Configure project'
CONFIGURE_PROJECT_SCRIPT_FILE_NAME = 'expo-configure-project.sh'
end
end

View File

@@ -0,0 +1,80 @@
module Expo
class PackagePod
# Name of the pod
attr_reader :pod_name
# The directory where the podspec is
attr_reader :podspec_dir
# Specification of the pod.
attr_reader :spec
def initialize(json)
@pod_name = json['podName']
@podspec_dir = json['podspecDir']
@spec = get_podspec_for_pod(self)
end
# Checks whether the podspec declares support for the given platform.
# It compares not only the platform name, but also the deployment target.
def supports_platform?(platform)
return platform && @spec.available_platforms().any? do |available_platform|
next platform.supports?(available_platform)
end
end
end # class PackagePod
class Package
# Name of the npm package
attr_reader :name
# Version of the npm package
attr_reader :version
# An array of pods found in the package
attr_reader :pods
# Flags to pass to the pod definition
attr_reader :flags
# Class names of the modules that need to be included in the generated modules provider.
attr_reader :modules
# Whether this module should only be added to the debug configuration.
attr_reader :debugOnly
# Names of Swift classes that hooks into `ExpoAppDelegate` to receive AppDelegate life-cycle events.
attr_reader :appDelegateSubscribers
# Names of Swift classes that implement `ExpoReactDelegateHandler` to hook React instance creation.
attr_reader :reactDelegateHandlers
def initialize(json)
@name = json['packageName']
@version = json['packageVersion']
@pods = json['pods'].map { |pod| PackagePod.new(pod) }
@flags = json.fetch('flags', {})
@modules = json.fetch('modules', [])
@debugOnly = json['debugOnly']
@appDelegateSubscribers = json.fetch('appDelegateSubscribers', [])
@reactDelegateHandlers = json.fetch('reactDelegateHandlers', [])
end
# Returns a boolean value whether the package has any module, app delegate subscriber or react delegate handler to link.
def has_something_to_link?
return !@modules.empty? || !@appDelegateSubscribers.empty? || !@reactDelegateHandlers.empty?
end
end # class Package
end # module Expo
private def get_podspec_for_pod(pod)
podspec_file_path = File.join(pod.podspec_dir, pod.pod_name + ".podspec")
return Pod::Specification.from_file(podspec_file_path)
end

View File

@@ -0,0 +1,15 @@
require 'singleton'
module Expo
# This class is used to store the configuration of the packages that are being used in the project.
# It is a singleton class, so it can be accessed from anywhere in the project.
class PackagesConfig
include Singleton
attr_accessor :coreFeatures
def initialize
@coreFeatures = []
end
end
end

View File

@@ -0,0 +1,308 @@
require 'fileutils'
require 'colored2'
module Expo
module ProjectIntegrator
include Pod
CONFIGURATION_FLAG_PREFIX = 'EXPO_CONFIGURATION_'
SWIFT_FLAGS = 'OTHER_SWIFT_FLAGS'
# Integrates targets in the project and generates modules providers.
def self.integrate_targets_in_project(targets, project)
# Find the targets that use expo modules and need the modules provider
targets_with_modules_provider = targets.select do |target|
autolinking_manager = target.target_definition.autolinking_manager
autolinking_manager.present? && autolinking_manager.should_generate_modules_provider?
end
# Find existing PBXGroup for modules providers.
generated_group = modules_providers_group(project, targets_with_modules_provider.any?)
# Return early when the modules providers group has not been auto-created in the line above.
return if generated_group.nil?
# Remove existing groups for targets without modules provider.
generated_group.groups.each do |group|
# Remove the group if there is no target for this group.
if targets.none? { |target| target.target_definition.name == group.name && targets_with_modules_provider.include?(target) }
recursively_remove_group(group)
end
end
targets_with_modules_provider.sort_by(&:name).each do |target|
# The user target name (without `Pods-` prefix which is a part of `target.name`)
target_name = target.target_definition.name
# PBXNativeTarget of the user target
native_target = project.native_targets.find { |native_target| native_target.name == target_name }
# Shorthand ref for the autolinking manager.
autolinking_manager = target.target_definition.autolinking_manager
UI.message '- Generating the provider for ' << target_name.green << ' target' do
# Get the absolute path to the modules provider
modules_provider_path = autolinking_manager.modules_provider_path(target)
# Run `expo-modules-autolinking` command to generate the file
autolinking_manager.generate_modules_provider(target_name, modules_provider_path)
# PBXGroup for generated files per target
generated_target_group = generated_group.find_subpath(target_name, true)
# PBXGroup uses relative paths, so we need to strip the absolute path
modules_provider_relative_path = Pathname.new(modules_provider_path).relative_path_from(generated_target_group.real_path).to_s
if generated_target_group.find_file_by_path(modules_provider_relative_path).nil?
# Create new PBXFileReference if the modules provider is not in the group yet
modules_provider_file_reference = generated_target_group.new_file(modules_provider_path)
if native_target.source_build_phase.files_references.find { |ref| ref.present? && ref.path == modules_provider_relative_path }.nil?
# Put newly created PBXFileReference to the source files of the native target
native_target.add_file_references([modules_provider_file_reference])
project.mark_dirty!
end
end
end
integrate_build_script(autolinking_manager, project, target, native_target)
end
# Remove the generated group if it has nothing left inside
if targets_with_modules_provider.empty?
recursively_remove_group(generated_group)
end
end
def self.recursively_remove_group(group)
return if group.nil?
UI.message '- Removing ' << group.name.green << ' group' do
group.recursive_children.each do |child|
UI.message ' - Removing a reference to ' << child.name.green
child.remove_from_project
end
group.remove_from_project
group.project.mark_dirty!
end
end
# CocoaPods doesn't properly remove file references from the build phase
# They appear as nils and it's safe to just delete them from native targets
def self.remove_nils_from_source_files(project)
project.native_targets.each do |native_target|
native_target.source_build_phase.files.each do |build_file|
next unless build_file.file_ref.nil?
build_file.remove_from_project
project.mark_dirty!
end
end
end
def self.modules_providers_group(project, autocreate = false)
project.main_group.find_subpath(Constants::GENERATED_GROUP_NAME, autocreate)
end
# Sets EXPO_CONFIGURATION_* compiler flag for Swift.
def self.set_autolinking_configuration(project)
project.native_targets.each do |native_target|
native_target.build_configurations.each do |build_configuration|
configuration_flag = "-D #{CONFIGURATION_FLAG_PREFIX}#{build_configuration.debug? ? "DEBUG" : "RELEASE"}"
build_settings = build_configuration.build_settings
# For some targets it might be `nil` by default which is an equivalent to `$(inherited)`
if build_settings[SWIFT_FLAGS].nil?
build_settings[SWIFT_FLAGS] ||= '$(inherited)'
end
# If the correct flag is not set yet
if !build_settings[SWIFT_FLAGS].include?(configuration_flag)
# Remove existing flag to make sure we don't put another one each time
build_settings[SWIFT_FLAGS] = build_settings[SWIFT_FLAGS].gsub(/\b-D\s+#{Regexp.quote(CONFIGURATION_FLAG_PREFIX)}\w+/, '')
# Add the correct flag
build_settings[SWIFT_FLAGS] << ' ' << configuration_flag
# Make sure the project will be saved as we did some changes
project.mark_dirty!
end
end
end
end
# Makes sure that the build script configuring the project is installed,
# is up-to-date and is placed before the "Compile Sources" phase.
def self.integrate_build_script(autolinking_manager, project, target, native_target)
build_phases = native_target.build_phases
modules_provider_path = autolinking_manager.modules_provider_path(target)
# Look for our own build script phase
xcode_build_script = native_target.shell_script_build_phases.find { |script|
script.name == Constants::CONFIGURE_PROJECT_BUILD_SCRIPT_NAME
}
if xcode_build_script.nil?
# Inform the user that we added a build script.
puts "[Expo] ".blue << "Installing the build script for target " << native_target.name.green
# Create a new build script in the target, it's added as the last phase
xcode_build_script = native_target.new_shell_script_build_phase(Constants::CONFIGURE_PROJECT_BUILD_SCRIPT_NAME)
end
# Make sure it is before the "Compile Sources" build phase
xcode_build_script_index = build_phases.find_index(xcode_build_script)
compile_sources_index = build_phases.find_index { |phase|
phase.is_a?(Xcodeproj::Project::PBXSourcesBuildPhase)
}
entitlement_path = nil
native_target.build_configurations.each do |build_configuration|
current_entitlement_path = build_configuration.build_settings['CODE_SIGN_ENTITLEMENTS']
unless current_entitlement_path
next
end
current_entitlement_path = File.join(project.project_dir, current_entitlement_path)
if !entitlement_path.nil? && entitlement_path != current_entitlement_path
Pod::UI.warn("Found multiple entitlement files in the build configurations of the target '#{native_target.name}' and using the first matched '#{current_entitlement_path}' for build")
next
end
entitlement_path = current_entitlement_path
end
if xcode_build_script_index.nil?
# This is almost impossible to get here as the script was just created with `new_shell_script_build_phase`
# that puts the script at the end of the phases, but let's log it just in case.
puts "[Expo] ".blue << "Unable to find the configuring build script in the Xcode project".red
end
if compile_sources_index.nil?
# In this case the project will probably not compile but that's not our fault
# and it doesn't block us from updating our build script.
puts "[Expo] ".blue << "Unable to find the compilation build phase in the Xcode project".red
end
# Insert our script before the "Compile Sources" phase when necessary
unless compile_sources_index.nil? || xcode_build_script_index < compile_sources_index
build_phases.insert(
compile_sources_index,
build_phases.delete_at(xcode_build_script_index)
)
end
# Get path to the script that will be added to the target support files
support_script_path = File.join(target.support_files_dir, Constants::CONFIGURE_PROJECT_SCRIPT_FILE_NAME)
support_script_relative_path = Pathname.new(support_script_path).relative_path_from(project.project_dir)
# Write to the shell script so it's always in-sync with the autolinking configuration
IO.write(
support_script_path,
generate_support_script(autolinking_manager, modules_provider_path, entitlement_path)
)
# Make the support script executable
FileUtils.chmod('+x', support_script_path)
# Force the build phase script to run on each build (including incremental builds)
xcode_build_script.always_out_of_date = '1'
# Make sure the build script in Xcode is up to date, but probably it's not going to change
# as it just runs the script generated in the target support files
xcode_build_script.shell_script = generate_xcode_build_script(support_script_relative_path)
modules_provider_relative_path = Pathname.new(modules_provider_path).relative_path_from(project.project_dir)
entitlement_relative_path = entitlement_path.nil? ? nil : Pathname.new(entitlement_path).relative_path_from(project.project_dir)
# Add input and output files to the build script phase to support ENABLE_USER_SCRIPT_SANDBOXING
xcode_build_script.input_paths = [
".xcode.env",
".xcode.env.local",
entitlement_relative_path,
support_script_relative_path,
].compact.map { |path| "$(SRCROOT)/#{path}" }
xcode_build_script.output_paths = [
"$(SRCROOT)/#{modules_provider_relative_path}",
]
end
# Generates the shell script of the build script phase.
# Try not to modify this since it involves changes in the pbxproj so
# it's better to modify the support script instead, if possible.
def self.generate_xcode_build_script(script_relative_path)
escaped_path = script_relative_path.to_s.gsub(/[^a-zA-Z0-9,\._\+@%\/\-]/) { |char| "\\#{char}" }
<<~XCODE_BUILD_SCRIPT
# This script configures Expo modules and generates the modules provider file.
bash -l -c "./#{escaped_path}"
XCODE_BUILD_SCRIPT
end
# Generates the support script that is executed by the build script phase.
def self.generate_support_script(autolinking_manager, modules_provider_path, entitlement_path)
args = autolinking_manager.base_command_args.map { |arg| "\"#{arg}\"" }
platform = autolinking_manager.platform_name.downcase
package_names = autolinking_manager.packages_to_generate.map { |package| "\"#{package.name}\"" }
entitlement_param = entitlement_path.nil? ? '' : "--entitlement \"#{entitlement_path}\""
app_root_param = autolinking_manager.custom_app_root.nil? ? '' : "--app-root \"#{autolinking_manager.custom_app_root}\""
<<~SUPPORT_SCRIPT
#!/usr/bin/env bash
# @generated by expo-modules-autolinking
set -eo pipefail
function with_node() {
# Start with a default
export NODE_BINARY=$(command -v node)
# Override the default with the global environment
ENV_PATH="$PODS_ROOT/../.xcode.env"
if [[ -f "$ENV_PATH" ]]; then
source "$ENV_PATH"
fi
# Override the global with the local environment
LOCAL_ENV_PATH="${ENV_PATH}.local"
if [[ -f "$LOCAL_ENV_PATH" ]]; then
source "$LOCAL_ENV_PATH"
fi
if [[ -n "$NODE_BINARY" && -x "$NODE_BINARY" ]]; then
echo "Node found at: ${NODE_BINARY}"
else
cat >&2 << NODE_NOT_FOUND
error: Could not find "node" executable while running an Xcode build script.
You need to specify the path to your Node.js executable by defining an environment variable named NODE_BINARY in your project's .xcode.env or .xcode.env.local file.
You can set this up quickly by running:
echo "export NODE_BINARY=\\$(command -v node)" >> .xcode.env
in the ios folder of your project.
NODE_NOT_FOUND
exit 1
fi
# Execute argument, if present
if [[ "$#" -gt 0 ]]; then
"$NODE_BINARY" "$@"
fi
}
with_node \\
--no-warnings \\
--eval "require(\'expo/bin/autolinking\')" \\
expo-modules-autolinking \\
generate-modules-provider #{args.join(' ')} \\
--target "#{modules_provider_path}" \\
#{entitlement_param} \\
#{app_root_param} \\
--platform "apple" \\
--packages #{package_names.join(' ')}
SUPPORT_SCRIPT
end
end # module ProjectIntegrator
end # module Expo

View File

@@ -0,0 +1,47 @@
require 'open3'
require 'pathname'
def generate_or_remove_xcode_env_updates_file!()
project_directory = Pod::Config.instance.project_root
xcode_env_file = File.join(project_directory, '.xcode.env.updates')
ex_updates_native_debug = ENV['EX_UPDATES_NATIVE_DEBUG'] == '1' ||
ENV['EX_UPDATES_NATIVE_DEBUG'] == 'true'
if ex_updates_native_debug
Pod::UI.info "EX_UPDATES_NATIVE_DEBUG is set; auto-generating `.xcode.env.updates` to disable packager and generate debug bundle"
if File.exist?(xcode_env_file)
File.delete(xcode_env_file)
end
File.write(xcode_env_file, <<~EOS
# These force Xcode to build the JS bundle with DEV=false
export FORCE_BUNDLING=1
unset SKIP_BUNDLING
export RCT_NO_LAUNCH_PACKAGER=1
export CONFIGURATION=Release
EOS
)
else
if File.exist?(xcode_env_file)
Pod::UI.info "EX_UPDATES_NATIVE_DEBUG has been unset; removing `.xcode.env.updates`"
File.delete(xcode_env_file)
end
end
end
def maybe_generate_xcode_env_file!()
project_directory = Pod::Config.instance.project_root
xcode_env_file = File.join(project_directory, '.xcode.env.local')
if File.exist?(xcode_env_file)
return
end
# Adding the meta character `;` at the end of command for Ruby `Kernel.exec` to execute the command in shell.
stdout, stderr, status = Open3.capture3('node --print "process.argv[0]";')
node_path = stdout.strip
if !stderr.empty? || status.exitstatus != 0 || node_path.empty?
Pod::UI.warn "Unable to generate `.xcode.env.local` for Node.js binary path: #{stderr}"
else
Pod::UI.info "Auto-generating `.xcode.env.local` with $NODE_BINARY=#{node_path}"
File.write(xcode_env_file, "export NODE_BINARY=\"#{node_path}\"\n")
end
end