require_relative "xcodebuild_raw" module PodPrebuild class XcodebuildCommand # rubocop:disable Metrics/ClassLength def initialize(options) @options = options case options[:targets][0].platform.name when :ios @options[:device] = "iphoneos" @options[:simulator] = "iphonesimulator" when :tvos @options[:device] = "appletvos" @options[:simulator] = "appletvsimulator" when :watchos @options[:device] = "watchos" @options[:simulator] = "watchsimulator" end @build_args = make_up_build_args(options[:args] || {}) end def run sdks.each { |sdk| build_for_sdk(sdk) } targets.each do |target| if PodPrebuild.config.xcframework? create_xcframework(target) elsif sdks.count > 1 create_fat_framework(target) else collect_output(target, Dir[target_products_dir_of(target, sdks[0]) + "/*"]) end end fix_swiftinterface_files(targets) end private def sdks @sdks ||= begin sdks_ = [] sdks_ << simulator if build_types.include?(:simulator) sdks_ << device if build_types.include?(:device) sdks_ end end def preferred_sdk @preferred_sdk ||= sdks.include?(device) ? device : sdks[0] end def build_types @build_types ||= begin # TODO (thuyen): Add DSL options `build_for_types` to specify build types types = [:simulator] types << :device if device_build_enabled? types end end def make_up_build_args(args) # Note: The build arguments explicitly passed from config_cocoapods_binary_cache # should be preceded by the default arguments so that they could take higher priority # when there are argument collisions in the xcodebuild command. # For ex. `xcodebuild AN_ARG=1 AN_ARG=2` should use `AN_ARG=2` instead. args_ = args.clone args_[:default] ||= [] args_[:simulator] ||= [] args_[:device] ||= [] args_[:default].prepend("BITCODE_GENERATION_MODE=bitcode") if bitcode_enabled? args_[:default].prepend("DEBUG_INFORMATION_FORMAT=dwarf-with-dsym") unless disable_dsym? args_[:default].prepend("DEBUG_INFORMATION_FORMAT=dwarf") if disable_dsym? args_[:default].prepend("BUILD_LIBRARY_FOR_DISTRIBUTION=YES") args_[:simulator].prepend("ARCHS=x86_64", "ONLY_ACTIVE_ARCH=NO") if simulator == "iphonesimulator" args_[:simulator] += args_[:default] args_[:device].prepend("ONLY_ACTIVE_ARCH=NO") args_[:device] += args_[:default] args_ end def build_for_sdk(sdk) PodPrebuild::XcodebuildCommand.xcodebuild( sandbox: sandbox, scheme: scheme, targets: targets.map(&:label), configuration: configuration, sdk: sdk, deployment_target: targets.map { |t| t.platform.deployment_target }.max.to_s, log_path: log_path(sdk), args: sdk == simulator ? @build_args[:simulator] : @build_args[:device] ) end def create_xcframework(target) non_framework_paths = Dir[target_products_dir_of(target, preferred_sdk) + "/*"] \ - [framework_path_of(target, preferred_sdk)] \ - dsym_paths_of(target, preferred_sdk) \ - bcsymbolmap_paths_of(target, preferred_sdk) collect_output(target, non_framework_paths) output = "#{output_path(target)}/#{target.product_module_name}.xcframework" FileUtils.rm_rf(output) cmd = ["xcodebuild", "-create-xcframework", "-allow-internal-distribution"] # for each sdk, the order of params must be -framework then -debug-symbols # to prevent duplicated file error when copying dSYMs sdks.each do |sdk| cmd << "-framework" << framework_path_of(target, sdk).shellescape unless disable_dsym? dsyms = dsym_paths_of(target, sdk) cmd += dsyms.map { |dsym| "-debug-symbols #{dsym.shellescape}" } end if bitcode_enabled? bcsymbolmaps = bcsymbolmap_paths_of(target, sdk) cmd += bcsymbolmaps.map { |bcsymbolmap| "-debug-symbols #{bcsymbolmap.shellescape}" } end end cmd << "-output" << output Pod::UI.puts "- Create xcframework: #{target}".magenta Pod::UI.puts_indented "$ #{cmd.join(' ')}" unless PodPrebuild.config.silent_build? `#{cmd.join(" ")}` end def fix_swiftinterface_files(targets) targets.each do |target| escapedPod = target.product_module_name.gsub('/', '\/') # replace when framework and class in framework with the same name replaceString1 = "-e '/^import/! s/ #{escapedPod}\\./ /g'" #remove framework name in swiftinterface of it frameworkd replaceString2 = "-e 's/\(#{escapedPod}\\./(/g'" replaceString3 = "-e 's/\\[#{escapedPod}\\./[/g'" `find "#{output_path(target)}/#{target.product_module_name}.xcframework" -name '*.swiftinterface' -exec sed -i -e #{replaceString1} #{replaceString2} #{replaceString3} {} \\\;` end #remove dublication name in other frameworks replaceString = targets.map { |x| x.product_module_name }.uniq().map{ |x| x.gsub('/', '\/')}.map{|x| "-e 's/#{x}\\.#{x}/#{x}/g'"}.join(' ') `find #{@options[:output_path]} -name '*.swiftinterface' -exec sed -i -e #{replaceString} {} \\\;` end def create_fat_framework(target) # When merging contents of `simulator` & `device`, prefer contents of `device` over `simulator` # https://github.com/grab/cocoapods-binary-cache/issues/25 collect_output(target, Dir[target_products_dir_of(target, device) + "/*"]) merge_framework_binary(target) merge_framework_dsym(target) merge_swift_headers(target) merge_swift_modules(target) end def merge_framework_binary(target) merge_contents(target, "/#{target.product_module_name}", &method(:create_fat_binary)) end def merge_framework_dsym(target) merge_contents( target, ".dSYM/Contents/Resources/DWARF/#{target.product_module_name}", &method(:create_fat_binary) ) end def merge_swift_headers(target) merge_contents(target, "/Headers/#{target.product_module_name}-Swift.h") do |options| merged_header = <<~HEREDOC #if TARGET_OS_SIMULATOR // merged by cocoapods-binary #{File.read(options[:simulator])} #else // merged by cocoapods-binary #{File.read(options[:device])} #endif // merged by cocoapods-binary HEREDOC File.write(options[:output], merged_header.strip) end end def merge_swift_modules(target) merge_contents(target, "/Modules/#{target.product_module_name}.swiftmodule") do |options| # Note: swiftmodules of `device` were copied beforehand, # here, we only need to copy swiftmodules of `simulator` FileUtils.cp_r(options[:simulator] + "/.", options[:output]) end end def merge_contents(target, path_suffix, &merger) simulator_, device_, output_ = [ framework_path_of(target, simulator), framework_path_of(target, device), "#{output_path(target)}/#{target.product_module_name}.framework" ].map { |p| p + path_suffix } return unless File.exist?(simulator_) && File.exist?(device_) merger.call(simulator: simulator_, device: device_, output: output_) end def create_fat_binary(options) cmd = ["lipo", " -create"] cmd << "-output" << options[:output] cmd << options[:simulator].shellescape << options[:device].shellescape `#{cmd.join(" ")}` end def collect_output(target, paths) FileUtils.mkdir_p(output_path(target)) paths = [paths] unless paths.is_a?(Array) paths.each do |path| FileUtils.rm_rf(File.join(output_path(target), File.basename(path))) FileUtils.cp_r(path, output_path(target)) end end def target_products_dir_of(target, sdk) "#{build_dir}/#{configuration}-#{sdk}/#{target.name}" end def framework_path_of(target, sdk) "#{target_products_dir_of(target, sdk)}/#{target.product_module_name}.framework" end def dsym_paths_of(target, sdk) Dir["#{target_products_dir_of(target, sdk)}/*.dSYM"] end def bcsymbolmap_paths_of(target, sdk) Dir["#{target_products_dir_of(target, sdk)}/*.bcsymbolmap"] end def sandbox @options[:sandbox] end def build_dir @options[:build_dir] end def output_path(target) "#{@options[:output_path]}/#{target.label}" end def scheme @options[:scheme] end def targets @options[:targets] end def configuration @options[:configuration] end def bitcode_enabled? @options[:bitcode_enabled] end def device_build_enabled? @options[:device_build_enabled] end def device @options[:device] || "iphoneos" end def simulator @options[:simulator] || "iphonesimulator" end def disable_dsym? @options[:disable_dsym] end def log_path(sdk) @options[:log_path].nil? ? nil : "#{@options[:log_path]}_#{sdk}" end end end