require 'zip' require 'plist' module Deliver class IpaUploaderError < StandardError end IPA_UPLOAD_STRATEGY_APP_STORE = 1 IPA_UPLOAD_STRATEGY_BETA_BUILD = 2 IPA_UPLOAD_STRATEGY_JUST_UPLOAD = 3 # This class takes care of preparing and uploading the given ipa file # Metadata + IPA file can not be handled in one file class IpaUploader < AppMetadata attr_accessor :app attr_accessor :publish_strategy # Create a new uploader for one ipa file. This will only upload the ipa and no # other app metadata. # @param app (Deliver::App) The app for which the ipa should be uploaded for # @param dir (String) The path to where we can store (copy) the ipa file. Usually /tmp/ # @param ipa_path (String) The path to the IPA file which should be uploaded # @param publish_strategy (Int) If it's a beta build, it will be released to the testers. # If it's a production build it will be released into production. Otherwise no action. # @raise (IpaUploaderError) Is thrown when the ipa file was not found or is not valid def initialize(app, dir, ipa_path, publish_strategy) ipa_path.strip! # remove unused white spaces raise IpaUploaderError.new("IPA on path '#{ipa_path}' not found") unless File.exists?(ipa_path) raise IpaUploaderError.new("IPA on path '#{ipa_path}' is not a valid IPA file") unless ipa_path.include?".ipa" super(app, dir, false) @ipa_file = Deliver::MetadataItem.new(ipa_path) @publish_strategy = publish_strategy end # Fetches the app identifier (e.g. com.facebook.Facebook) from the given ipa file. def fetch_app_identifier plist = fetch_info_plist_file return plist['CFBundleIdentifier'] if plist return nil end # Fetches the app version from the given ipa file. def fetch_app_version plist = fetch_info_plist_file return plist['CFBundleShortVersionString'] if plist return nil end ##################################################### # @!group Uploading the ipa file ##################################################### # Actually upload the ipa file to Apple # @param submit_information (Hash) A hash containing submit information (export, content rights) def upload!(submit_information = nil) Helper.log.info "Uploading ipa file to iTunesConnect" build_document # Write the current XML state to disk folder_name = "#{@app.apple_id}.itmsp" path = "#{@metadata_dir}/#{folder_name}/" FileUtils.mkdir_p path File.write("#{path}/#{METADATA_FILE_NAME}", @data.to_xml) @ipa_file.store_file_inside_package(path) is_okay = true begin transporter.upload(@app, @metadata_dir) rescue Exception => ex is_okay = ex.to_s.include?"ready exists a binary upload with build" # this just means, the ipa is already online end if is_okay unless Helper.is_test? `rm -rf ./#{@app.apple_id}.itmsp` # we don't need that any more return publish_on_itunes_connect(submit_information) end end return is_okay end private # This method will trigger the iTunesConnect class to choose the latest build def publish_on_itunes_connect(submit_information = nil) if @publish_strategy == IPA_UPLOAD_STRATEGY_APP_STORE return publish_production_build(submit_information) elsif @publish_strategy == IPA_UPLOAD_STRATEGY_BETA_BUILD return publish_beta_build end return false end def publish_beta_build # Distribute to beta testers Helper.log.info "Distributing the latest build to Beta Testers." if self.app.itc.put_build_into_beta_testing!(self.app, self.fetch_app_version) Helper.log.info "Successfully distributed a new beta build of your app.".green return true end return false end def publish_production_build(submit_information) # Publish onto Production Helper.log.info "Putting the latest build onto production." if self.app.itc.put_build_into_production!(self.app, self.fetch_app_version) if self.app.itc.submit_for_review!(self.app, submit_information) Helper.log.info "Successfully deployed a new update of your app. You can now enjoy a good cold Club Mate.".green return true end end return false end def build_document builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| xml.package(xmlns: "http://apple.com/itunes/importer", version: "software4.7") { xml.software_assets(apple_id: @app.apple_id) { xml.asset(type: 'bundle') { } } } end @data = builder.doc asset = @data.xpath('//x:asset', "x" => Deliver::AppMetadata::ITUNES_NAMESPACE).first asset << @ipa_file.create_xml_node(@data) end def fetch_info_plist_file Zip::File.open(@ipa_file.path) do |zipfile| zipfile.each do |file| if file.name.include?'.plist' and not ['.bundle', '.framework'].any? { |a| file.name.include?a } # We can not be completely sure, that's the correct plist file, so we have to try begin # The XML file has to be properly unpacked first tmp_path = "/tmp/deploytmp.plist" File.write(tmp_path, zipfile.read(file)) system("plutil -convert xml1 #{tmp_path}") result = Plist::parse_xml(tmp_path) File.delete(tmp_path) if result['CFBundleIdentifier'] or result['CFBundleVersion'] return result end rescue # We don't really care, look for another XML file end end end end nil end end end