309 lines
13 KiB
Ruby
309 lines
13 KiB
Ruby
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
|