diff --git a/README.md b/README.md index e0a6ade..7920bdb 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,28 @@ output_directory "./build" # store the ipa in this folder output_name "MyApp" # the name of the ipa file ``` +## Export options + +Since Xcode 7, `gym` is using new Xcode API which allows us to specify export options using `plist` file. By default `gym` creates this file for you and you are able to modify some parameters by using `export_method`, `export_team_id`, `include_symbols` or `include_bitcode`. If you want to have more options, like creating manifest file or app thinning, you can provide your own `plist` file: + +```ruby +export_options "./ExportOptions.plist" +``` + +or you can provide hash of values directly in the `Gymfile`: + +```ruby +export_options( + method: "ad-hoc", + manifest: { + appURL: "https://example.com/My App.ipa", + }, + thinning: "" +) +``` + +For the list of available options run `xcodebuild -help`. + # Automating the whole process `gym` works great together with [fastlane](https://fastlane.tools), which connects all deployment tools into one streamlined workflow. diff --git a/examples/standard/ExampleExport.plist b/examples/standard/ExampleExport.plist new file mode 100644 index 0000000..6c136c5 --- /dev/null +++ b/examples/standard/ExampleExport.plist @@ -0,0 +1,19 @@ + + + + + embedOnDemandResourcesAssetPacksInBundle + + manifest + + appURL + https://www.example.com/Example.ipa + displayImageURL + https://www.example.com/display.png + fullSizeImageURL + https://www.example.com/fullSize.png + + method + ad-hoc + + diff --git a/lib/gym/detect_values.rb b/lib/gym/detect_values.rb index 13a8c6f..71c5a96 100644 --- a/lib/gym/detect_values.rb +++ b/lib/gym/detect_values.rb @@ -27,9 +27,6 @@ def self.set_additional_default_values config[:output_name] ||= Gym.project.app_name - # we do it here, since the value is optional and should be pre-filled by fastlane if necessary - config[:export_method] ||= "app-store" - return config end diff --git a/lib/gym/generators/package_command_generator.rb b/lib/gym/generators/package_command_generator.rb index 30e03fe..20a636d 100644 --- a/lib/gym/generators/package_command_generator.rb +++ b/lib/gym/generators/package_command_generator.rb @@ -29,6 +29,22 @@ def dsym_path generator.dsym_path end + def manifest_path + generator.manifest_path + end + + def app_thinning_path + generator.app_thinning_path + end + + def app_thinning_size_report_path + generator.app_thinning_size_report_path + end + + def apps_path + generator.apps_path + end + # The generator we need to use for the currently used Xcode version def generator if Gym.config[:use_legacy_build_api] diff --git a/lib/gym/generators/package_command_generator_legacy.rb b/lib/gym/generators/package_command_generator_legacy.rb index 25b2a7e..624e3c1 100644 --- a/lib/gym/generators/package_command_generator_legacy.rb +++ b/lib/gym/generators/package_command_generator_legacy.rb @@ -56,6 +56,22 @@ def ipa_path def dsym_path Dir[BuildCommandGenerator.archive_path + "/**/*.app.dSYM"].last end + + def manifest_path + "" + end + + def app_thinning_path + "" + end + + def app_thinning_size_report_path + "" + end + + def apps_path + "" + end end end end diff --git a/lib/gym/generators/package_command_generator_xcode7.rb b/lib/gym/generators/package_command_generator_xcode7.rb index fae7cb8..5c27725 100644 --- a/lib/gym/generators/package_command_generator_xcode7.rb +++ b/lib/gym/generators/package_command_generator_xcode7.rb @@ -43,10 +43,20 @@ def temporary_output_path def ipa_path unless Gym.cache[:ipa_path] path = Dir[File.join(temporary_output_path, "*.ipa")].last - ErrorHandler.handle_empty_archive unless path - - Gym.cache[:ipa_path] = File.join(temporary_output_path, "#{Gym.config[:output_name]}.ipa") - FileUtils.mv(path, Gym.cache[:ipa_path]) if File.expand_path(path).downcase != File.expand_path(Gym.cache[:ipa_path]).downcase + if path + # Try to find IPA file in the output directory, used when app thinning was not set + Gym.cache[:ipa_path] = File.join(temporary_output_path, "#{Gym.config[:output_name]}.ipa") + FileUtils.mv(path, Gym.cache[:ipa_path]) if File.expand_path(path).downcase != File.expand_path(Gym.cache[:ipa_path]).downcase + elsif Dir.exist?(apps_path) + # Try to find "generic" IPA file inside "Apps" folder, used when app thinning was set + files = Dir[File.join(apps_path, "*.ipa")] + # Generic IPA file doesn't have suffix so its name is the shortest + path = files.min_by(&:length) + Gym.cache[:ipa_path] = File.join(temporary_output_path, "#{Gym.config[:output_name]}.ipa") + FileUtils.cp(path, Gym.cache[:ipa_path]) if File.expand_path(path).downcase != File.expand_path(Gym.cache[:ipa_path]).downcase + else + ErrorHandler.handle_empty_archive unless path + end end Gym.cache[:ipa_path] end @@ -62,16 +72,87 @@ def config_path return Gym.cache[:config_path] end + # The path to the manifest plist file + def manifest_path + Gym.cache[:manifest_path] ||= File.join(temporary_output_path, "manifest.plist") + end + + # The path to the app-thinning plist file + def app_thinning_path + Gym.cache[:app_thinning] ||= File.join(temporary_output_path, "app-thinning.plist") + end + + # The path to the App Thinning Size Report file + def app_thinning_size_report_path + Gym.cache[:app_thinning_size_report] ||= File.join(temporary_output_path, "App Thinning Size Report.txt") + end + + # The path to the Apps folder + def apps_path + Gym.cache[:apps_path] ||= File.join(temporary_output_path, "Apps") + end + private + def normalize_export_options(hash) + # Normalize some values + hash[:onDemandResourcesAssetPacksBaseURL] = URI.escape(hash[:onDemandResourcesAssetPacksBaseURL]) if hash[:onDemandResourcesAssetPacksBaseURL] + if hash[:manifest] + hash[:manifest][:appURL] = URI.escape(hash[:manifest][:appURL]) if hash[:manifest][:appURL] + hash[:manifest][:displayImageURL] = URI.escape(hash[:manifest][:displayImageURL]) if hash[:manifest][:displayImageURL] + hash[:manifest][:fullSizeImageURL] = URI.escape(hash[:manifest][:fullSizeImageURL]) if hash[:manifest][:fullSizeImageURL] + hash[:manifest][:assetPackManifestURL] = URI.escape(hash[:manifest][:assetPackManifestURL]) if hash[:manifest][:assetPackManifestURL] + end + hash + end + + def keys_to_symbols(hash) + # Convert keys to symbols + hash = hash.each_with_object({}) do |(k, v), memo| + memo[k.to_sym] = v + memo + end + hash + end + + def read_export_options + # Reads export options + if Gym.config[:export_options] + if Gym.config[:export_options].kind_of?(Hash) + # Reads options from hash + hash = normalize_export_options(Gym.config[:export_options]) + else + # Reads optoins from file + hash = Plist.parse_xml(Gym.config[:export_options]) + # Convert keys to symbols + hash = keys_to_symbols(hash) + end + + # Saves configuration for later use + Gym.config[:export_method] ||= hash[:method] + Gym.config[:include_symbols] = hash[:uploadSymbols] if Gym.config[:include_symbols].nil? + Gym.config[:include_bitcode] = hash[:uploadBitcode] if Gym.config[:include_bitcode].nil? + Gym.config[:export_team_id] ||= hash[:teamID] + else + hash = {} + # Sets default values + Gym.config[:export_method] ||= "app-store" + Gym.config[:include_symbols] = true if Gym.config[:include_symbols].nil? + Gym.config[:include_bitcode] = false if Gym.config[:include_bitcode].nil? + end + hash + end + def config_content require 'plist' - hash = { method: Gym.config[:export_method] } + hash = read_export_options + # Overrides export options if needed + hash[:method] = Gym.config[:export_method] if Gym.config[:export_method] == 'app-store' - hash[:uploadSymbols] = (Gym.config[:include_symbols] ? true : false) - hash[:uploadBitcode] = (Gym.config[:include_bitcode] ? true : false) + hash[:uploadSymbols] = (Gym.config[:include_symbols] ? true : false) unless Gym.config[:include_symbols].nil? + hash[:uploadBitcode] = (Gym.config[:include_bitcode] ? true : false) unless Gym.config[:include_bitcode].nil? end hash[:teamID] = Gym.config[:export_team_id] if Gym.config[:export_team_id] diff --git a/lib/gym/options.rb b/lib/gym/options.rb index 096a757..2bdad1a 100644 --- a/lib/gym/options.rb +++ b/lib/gym/options.rb @@ -85,14 +85,14 @@ def self.plain_options short_option: "-m", env_name: "GYM_INCLUDE_SYMBOLS", description: "Should the ipa file include symbols?", - default_value: true, - is_string: false), + is_string: false, + optional: true), FastlaneCore::ConfigItem.new(key: :include_bitcode, short_option: "-z", env_name: "GYM_INCLUDE_BITCODE", description: "Should the ipa include bitcode?", - default_value: false, - is_string: false), + is_string: false, + optional: true), FastlaneCore::ConfigItem.new(key: :use_legacy_build_api, env_name: "GYM_USE_LEGACY_BUILD_API", description: "Don't use the new API because of https://openradar.appspot.com/radar?id=4952000420642816", @@ -113,6 +113,15 @@ def self.plain_options av = %w(app-store ad-hoc package enterprise development developer-id) UI.user_error!("Unsupported export_method, must be: #{av}") unless av.include?(value) end), + FastlaneCore::ConfigItem.new(key: :export_options, + env_name: "GYM_EXPORT_OPTIONS", + description: "Specifies path to export options plist. User xcodebuild -help to print the full set of available options", + is_string: false, + optional: true, + conflicting_options: [:use_legacy_build_api], + conflict_block: proc do |value| + UI.user_error!("'#{value.key}' must be false to use 'export_options'") + end), # Very optional FastlaneCore::ConfigItem.new(key: :archive_path, diff --git a/lib/gym/runner.rb b/lib/gym/runner.rb index b562bc1..931dc0e 100644 --- a/lib/gym/runner.rb +++ b/lib/gym/runner.rb @@ -17,7 +17,13 @@ def run package_app fix_package compress_and_move_dsym - move_ipa + path = move_ipa + move_manifest + move_app_thinning + move_app_thinning_size_report + move_apps_folder + + path elsif Gym.project.mac? compress_and_move_dsym copy_mac_app @@ -148,6 +154,54 @@ def copy_mac_app app_path end + # Move the manifest.plist if exists into the output directory + def move_manifest + if File.exist?(PackageCommandGenerator.manifest_path) + FileUtils.mv(PackageCommandGenerator.manifest_path, File.expand_path(Gym.config[:output_directory]), force: true) + manifest_path = File.join(File.expand_path(Gym.config[:output_directory]), File.basename(PackageCommandGenerator.manifest_path)) + + UI.success "Successfully exported the manifest.plist file:" + UI.message manifest_path + manifest_path + end + end + + # Move the app-thinning.plist file into the output directory + def move_app_thinning + if File.exist?(PackageCommandGenerator.app_thinning_path) + FileUtils.mv(PackageCommandGenerator.app_thinning_path, File.expand_path(Gym.config[:output_directory]), force: true) + app_thinning_path = File.join(File.expand_path(Gym.config[:output_directory]), File.basename(PackageCommandGenerator.app_thinning_path)) + + UI.success "Successfully exported the app-thinning.plist file:" + UI.message app_thinning_path + app_thinning_path + end + end + + # Move the App Thinning Size Report.txt file into the output directory + def move_app_thinning_size_report + if File.exist?(PackageCommandGenerator.app_thinning_size_report_path) + FileUtils.mv(PackageCommandGenerator.app_thinning_size_report_path, File.expand_path(Gym.config[:output_directory]), force: true) + app_thinning_size_report_path = File.join(File.expand_path(Gym.config[:output_directory]), File.basename(PackageCommandGenerator.app_thinning_size_report_path)) + + UI.success "Successfully exported the App Thinning Size Report.txt file:" + UI.message app_thinning_size_report_path + app_thinning_size_report_path + end + end + + # Move the Apps folder to the output directory + def move_apps_folder + if Dir.exist?(PackageCommandGenerator.apps_path) + FileUtils.mv(PackageCommandGenerator.apps_path, File.expand_path(Gym.config[:output_directory]), force: true) + apps_path = File.join(File.expand_path(Gym.config[:output_directory]), File.basename(PackageCommandGenerator.apps_path)) + + UI.success "Successfully exported Apps folder:" + UI.message apps_path + apps_path + end + end + private def find_archive_path diff --git a/spec/package_command_generator_xcode7_spec.rb b/spec/package_command_generator_xcode7_spec.rb index da3e74c..8486874 100644 --- a/spec/package_command_generator_xcode7_spec.rb +++ b/spec/package_command_generator_xcode7_spec.rb @@ -29,6 +29,98 @@ }) end + it "reads user export plist" do + options = { project: "./examples/standard/Example.xcodeproj", export_options: "./examples/standard/ExampleExport.plist" } + Gym.config = FastlaneCore::Configuration.create(Gym::Options.available_options, options) + + result = Gym::PackageCommandGeneratorXcode7.generate + config_path = Gym::PackageCommandGeneratorXcode7.config_path + + require 'plist' + expect(Plist.parse_xml(config_path)).to eq({ + 'embedOnDemandResourcesAssetPacksInBundle' => true, + 'manifest' => { + 'appURL' => 'https://www.example.com/Example.ipa', + 'displayImageURL' => 'https://www.example.com/display.png', + 'fullSizeImageURL' => 'https://www.example.com/fullSize.png' + }, + 'method' => 'ad-hoc' + }) + expect(Gym.config[:export_method]).to eq("ad-hoc") + expect(Gym.config[:include_symbols]).to be_nil + expect(Gym.config[:include_bitcode]).to be_nil + expect(Gym.config[:export_team_id]).to be_nil + end + + it "reads user export plist and override some parameters" do + options = { + project: "./examples/standard/Example.xcodeproj", + export_options: "./examples/standard/ExampleExport.plist", + export_method: "app-store", + include_symbols: false, + include_bitcode: true, + export_team_id: "1234567890" + } + Gym.config = FastlaneCore::Configuration.create(Gym::Options.available_options, options) + + result = Gym::PackageCommandGeneratorXcode7.generate + config_path = Gym::PackageCommandGeneratorXcode7.config_path + + require 'plist' + expect(Plist.parse_xml(config_path)).to eq({ + 'embedOnDemandResourcesAssetPacksInBundle' => true, + 'manifest' => { + 'appURL' => 'https://www.example.com/Example.ipa', + 'displayImageURL' => 'https://www.example.com/display.png', + 'fullSizeImageURL' => 'https://www.example.com/fullSize.png' + }, + 'method' => 'app-store', + 'uploadSymbols' => false, + 'uploadBitcode' => true, + 'teamID' => '1234567890' + }) + end + + it "reads export options from hash" do + options = { + project: "./examples/standard/Example.xcodeproj", + export_options: { + embedOnDemandResourcesAssetPacksInBundle: false, + manifest: { + appURL: "https://example.com/My App.ipa", + displayImageURL: "https://www.example.com/display image.png", + fullSizeImageURL: "https://www.example.com/fullSize image.png" + }, + method: "enterprise", + uploadSymbols: false, + uploadBitcode: true, + teamID: "1234567890" + }, + export_method: "app-store", + include_symbols: true, + include_bitcode: false, + export_team_id: "ASDFGHJK" + } + Gym.config = FastlaneCore::Configuration.create(Gym::Options.available_options, options) + + result = Gym::PackageCommandGeneratorXcode7.generate + config_path = Gym::PackageCommandGeneratorXcode7.config_path + + require 'plist' + expect(Plist.parse_xml(config_path)).to eq({ + 'embedOnDemandResourcesAssetPacksInBundle' => false, + 'manifest' => { + 'appURL' => 'https://example.com/My%20App.ipa', + 'displayImageURL' => 'https://www.example.com/display%20image.png', + 'fullSizeImageURL' => 'https://www.example.com/fullSize%20image.png' + }, + 'method' => 'app-store', + 'uploadSymbols' => true, + 'uploadBitcode' => false, + 'teamID' => 'ASDFGHJK' + }) + end + it "doesn't store bitcode/symbols information for non app-store builds" do options = { project: "./examples/standard/Example.xcodeproj", export_method: 'ad-hoc' } Gym.config = FastlaneCore::Configuration.create(Gym::Options.available_options, options) @@ -48,6 +140,10 @@ result = Gym::PackageCommandGeneratorXcode7.generate expect(Gym::PackageCommandGeneratorXcode7.temporary_output_path).to match(%r{#{Dir.tmpdir}/gym.+\.gym_output}) + expect(Gym::PackageCommandGeneratorXcode7.manifest_path).to match(%r{#{Dir.tmpdir}/gym.+\.gym_output/manifest.plist}) + expect(Gym::PackageCommandGeneratorXcode7.app_thinning_path).to match(%r{#{Dir.tmpdir}/gym.+\.gym_output/app-thinning.plist}) + expect(Gym::PackageCommandGeneratorXcode7.app_thinning_size_report_path).to match(%r{#{Dir.tmpdir}/gym.+\.gym_output/App Thinning Size Report.txt}) + expect(Gym::PackageCommandGeneratorXcode7.apps_path).to match(%r{#{Dir.tmpdir}/gym.+\.gym_output/Apps}) end end end