1require 'fileutils' 2require 'colored2' 3 4module Expo 5 module ProjectIntegrator 6 include Pod 7 8 CONFIGURATION_FLAG_PREFIX = 'EXPO_CONFIGURATION_' 9 SWIFT_FLAGS = 'OTHER_SWIFT_FLAGS' 10 11 # Integrates targets in the project and generates modules providers. 12 def self.integrate_targets_in_project(targets, project) 13 # Find the targets that use expo modules and need the modules provider 14 targets_with_modules_provider = targets.select do |target| 15 autolinking_manager = target.target_definition.autolinking_manager 16 autolinking_manager.present? && autolinking_manager.should_generate_modules_provider? 17 end 18 19 # Find existing PBXGroup for modules providers. 20 generated_group = modules_providers_group(project, targets_with_modules_provider.any?) 21 22 # Return early when the modules providers group has not been auto-created in the line above. 23 return if generated_group.nil? 24 25 # Remove existing groups for targets without modules provider. 26 generated_group.groups.each do |group| 27 # Remove the group if there is no target for this group. 28 if targets.none? { |target| target.target_definition.name == group.name && targets_with_modules_provider.include?(target) } 29 recursively_remove_group(group) 30 end 31 end 32 33 targets_with_modules_provider.sort_by(&:name).each do |target| 34 # The user target name (without `Pods-` prefix which is a part of `target.name`) 35 target_name = target.target_definition.name 36 37 # PBXNativeTarget of the user target 38 native_target = project.native_targets.find { |native_target| native_target.name == target_name } 39 40 # Shorthand ref for the autolinking manager. 41 autolinking_manager = target.target_definition.autolinking_manager 42 43 UI.message '- Generating the provider for ' << target_name.green << ' target' do 44 # Get the absolute path to the modules provider 45 modules_provider_path = autolinking_manager.modules_provider_path(target) 46 47 # Run `expo-modules-autolinking` command to generate the file 48 autolinking_manager.generate_package_list(target_name, modules_provider_path) 49 50 # PBXGroup for generated files per target 51 generated_target_group = generated_group.find_subpath(target_name, true) 52 53 # PBXGroup uses relative paths, so we need to strip the absolute path 54 modules_provider_relative_path = Pathname.new(modules_provider_path).relative_path_from(generated_target_group.real_path).to_s 55 56 if generated_target_group.find_file_by_path(modules_provider_relative_path).nil? 57 # Create new PBXFileReference if the modules provider is not in the group yet 58 modules_provider_file_reference = generated_target_group.new_file(modules_provider_path) 59 60 if native_target.source_build_phase.files_references.find { |ref| ref.present? && ref.path == modules_provider_relative_path }.nil? 61 # Put newly created PBXFileReference to the source files of the native target 62 native_target.add_file_references([modules_provider_file_reference]) 63 project.mark_dirty! 64 end 65 end 66 end 67 68 integrate_build_script(autolinking_manager, project, target, native_target) 69 end 70 71 # Remove the generated group if it has nothing left inside 72 if targets_with_modules_provider.empty? 73 recursively_remove_group(generated_group) 74 end 75 end 76 77 def self.recursively_remove_group(group) 78 return if group.nil? 79 80 UI.message '- Removing ' << group.name.green << ' group' do 81 group.recursive_children.each do |child| 82 UI.message ' - Removing a reference to ' << child.name.green 83 child.remove_from_project 84 end 85 86 group.remove_from_project 87 group.project.mark_dirty! 88 end 89 end 90 91 # CocoaPods doesn't properly remove file references from the build phase 92 # They appear as nils and it's safe to just delete them from native targets 93 def self.remove_nils_from_source_files(project) 94 project.native_targets.each do |native_target| 95 native_target.source_build_phase.files.each do |build_file| 96 next unless build_file.file_ref.nil? 97 98 build_file.remove_from_project 99 project.mark_dirty! 100 end 101 end 102 end 103 104 def self.modules_providers_group(project, autocreate = false) 105 project.main_group.find_subpath(Constants::GENERATED_GROUP_NAME, autocreate) 106 end 107 108 # Sets EXPO_CONFIGURATION_* compiler flag for Swift. 109 def self.set_autolinking_configuration(project) 110 project.native_targets.each do |native_target| 111 native_target.build_configurations.each do |build_configuration| 112 configuration_flag = "-D #{CONFIGURATION_FLAG_PREFIX}#{build_configuration.debug? ? "DEBUG" : "RELEASE"}" 113 build_settings = build_configuration.build_settings 114 115 # For some targets it might be `nil` by default which is an equivalent to `$(inherited)` 116 if build_settings[SWIFT_FLAGS].nil? 117 build_settings[SWIFT_FLAGS] ||= '$(inherited)' 118 end 119 120 # If the correct flag is not set yet 121 if !build_settings[SWIFT_FLAGS].include?(configuration_flag) 122 # Remove existing flag to make sure we don't put another one each time 123 build_settings[SWIFT_FLAGS] = build_settings[SWIFT_FLAGS].gsub(/\b-D\s+#{Regexp.quote(CONFIGURATION_FLAG_PREFIX)}\w+/, '') 124 125 # Add the correct flag 126 build_settings[SWIFT_FLAGS] << ' ' << configuration_flag 127 128 # Make sure the project will be saved as we did some changes 129 project.mark_dirty! 130 end 131 end 132 end 133 end 134 135 # Makes sure that the build script configuring the project is installed, 136 # is up-to-date and is placed before the "Compile Sources" phase. 137 def self.integrate_build_script(autolinking_manager, project, target, native_target) 138 build_phases = native_target.build_phases 139 modules_provider_path = autolinking_manager.modules_provider_path(target) 140 141 # Look for our own build script phase 142 xcode_build_script = native_target.shell_script_build_phases.find { |script| 143 script.name == Constants::CONFIGURE_PROJECT_BUILD_SCRIPT_NAME 144 } 145 146 if xcode_build_script.nil? 147 # Inform the user that we added a build script. 148 puts "[Expo] ".blue << "Installing the build script for target " << native_target.name.green 149 150 # Create a new build script in the target, it's added as the last phase 151 xcode_build_script = native_target.new_shell_script_build_phase(Constants::CONFIGURE_PROJECT_BUILD_SCRIPT_NAME) 152 end 153 154 # Make sure it is before the "Compile Sources" build phase 155 xcode_build_script_index = build_phases.find_index(xcode_build_script) 156 compile_sources_index = build_phases.find_index { |phase| 157 phase.is_a?(Xcodeproj::Project::PBXSourcesBuildPhase) 158 } 159 160 if xcode_build_script_index.nil? 161 # This is almost impossible to get here as the script was just created with `new_shell_script_build_phase` 162 # that puts the script at the end of the phases, but let's log it just in case. 163 puts "[Expo] ".blue << "Unable to find the configuring build script in the Xcode project".red 164 end 165 166 if compile_sources_index.nil? 167 # In this case the project will probably not compile but that's not our fault 168 # and it doesn't block us from updating our build script. 169 puts "[Expo] ".blue << "Unable to find the compilation build phase in the Xcode project".red 170 end 171 172 # Insert our script before the "Compile Sources" phase when necessary 173 unless compile_sources_index.nil? || xcode_build_script_index < compile_sources_index 174 build_phases.insert( 175 compile_sources_index, 176 build_phases.delete_at(xcode_build_script_index) 177 ) 178 end 179 180 # Get path to the script that will be added to the target support files 181 support_script_path = File.join(target.support_files_dir, Constants::CONFIGURE_PROJECT_SCRIPT_FILE_NAME) 182 support_script_relative_path = Pathname.new(support_script_path).relative_path_from(project.project_dir) 183 184 # Write to the shell script so it's always in-sync with the autolinking configuration 185 IO.write( 186 support_script_path, 187 generate_support_script(autolinking_manager, modules_provider_path) 188 ) 189 190 # Make the support script executable 191 FileUtils.chmod('+x', support_script_path) 192 193 # Force the build phase script to run on each build (including incremental builds) 194 xcode_build_script.always_out_of_date = '1' 195 196 # Make sure the build script in Xcode is up to date, but probably it's not going to change 197 # as it just runs the script generated in the target support files 198 xcode_build_script.shell_script = generate_xcode_build_script(support_script_relative_path) 199 end 200 201 # Generates the shell script of the build script phase. 202 # Try not to modify this since it involves changes in the pbxproj so 203 # it's better to modify the support script instead, if possible. 204 def self.generate_xcode_build_script(script_relative_path) 205 escaped_path = script_relative_path.to_s.gsub(/[^a-zA-Z0-9,\._\+@%\/\-]/) { |char| "\\#{char}" } 206 207 <<~XCODE_BUILD_SCRIPT 208 # This script configures Expo modules and generates the modules provider file. 209 bash -l -c "./#{escaped_path}" 210 XCODE_BUILD_SCRIPT 211 end 212 213 # Generates the support script that is executed by the build script phase. 214 def self.generate_support_script(autolinking_manager, modules_provider_path) 215 args = autolinking_manager.base_command_args.map { |arg| "\"#{arg}\"" }.join(' ') 216 217 <<~SUPPORT_SCRIPT 218 #!/usr/bin/env bash 219 # @generated by expo-modules-autolinking 220 221 set -eo pipefail 222 223 function with_node() { 224 # Start with a default 225 export NODE_BINARY=$(command -v node) 226 227 # Override the default with the global environment 228 ENV_PATH="$PODS_ROOT/../.xcode.env" 229 if [[ -f "$ENV_PATH" ]]; then 230 source "$ENV_PATH" 231 fi 232 233 # Override the global with the local environment 234 LOCAL_ENV_PATH="${ENV_PATH}.local" 235 if [[ -f "$LOCAL_ENV_PATH" ]]; then 236 source "$LOCAL_ENV_PATH" 237 fi 238 239 if [[ -n "$NODE_BINARY" && -x "$NODE_BINARY" ]]; then 240 echo "Node found at: ${NODE_BINARY}" 241 else 242 cat >&2 << NODE_NOT_FOUND 243 error: Could not find "node" executable while running an Xcode build script. 244 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. 245 You can set this up quickly by running: 246 247 echo "export NODE_BINARY=\\$(command -v node)" >> .xcode.env 248 249 in the ios folder of your project. 250 NODE_NOT_FOUND 251 252 exit 1 253 fi 254 255 # Execute argument, if present 256 if [[ "$#" -gt 0 ]]; then 257 "$NODE_BINARY" "$@" 258 fi 259 } 260 261 with_node --no-warnings --eval "require(\'expo-modules-autolinking\')(process.argv.slice(1))" generate-package-list #{args} --target "#{modules_provider_path}" 262 SUPPORT_SCRIPT 263 end 264 265 end # module ProjectIntegrator 266end # module Expo 267