#!/usr/bin/env ruby require 'rubygems' require 'zip/zip' require 'digest' require 'rest_client' require 'json' require 'rbconfig' require 'tmpdir' def host ENV["LP_HOST"] || "https://www.lesspainful.com" end def digest(file) Digest::SHA256.file(file).hexdigest end def unzip_file (file, destination) Zip::ZipFile.open(file) { |zip_file| zip_file.each { |f| f_path=File.join(destination, f.name) FileUtils.mkdir_p(File.dirname(f_path)) zip_file.extract(f, f_path) unless File.exist?(f_path) } } end def workspace if ARGV[2] abort_unless(File.exist?(ARGV[2])) do puts "Provided workspace: #{ARGV[2]} does not exist." end File.join(File.expand_path(ARGV[2]),File::Separator) else "" end end def features_zip if ARGV[3] and ARGV[3].end_with?".zip" abort_unless(File.exist?(ARGV[3])) do puts "No file found #{ARGV[3]}" end File.expand_path(ARGV[3]) end end def app ARGV[0] end def api_key ARGV[1] end def is_android? app.end_with? ".apk" end def calabash_android_version `bundle exec calabash-android version`.strip end def is_ios? app.end_with? ".ipa" end def test_server_path require 'digest/md5' digest = Digest::MD5.file(app).hexdigest File.join("test_servers","#{digest}_#{calabash_android_version}.apk") end def all_files dir = workspace if features_zip dir = Dir.mktmpdir unzip_file(features_zip, dir) dir = File.join(dir,File::Separator) end files = Dir.glob(File.join("#{dir}features","**","*")) if File.directory?("#{dir}playback") files += Dir.glob(File.join("#{dir}playback","*")) end files += Dir.glob(File.join("#{workspace}vendor","cache","*")) if workspace and workspace.strip != "" files += Dir.glob("#{workspace}Gemfile") files += Dir.glob("#{workspace}Gemfile.lock") end if is_android? files << test_server_path end p files {:feature_prefix => dir, :workspace_prefix => workspace, :files => files.find_all { |file_or_dir| File.file? file_or_dir }} end def http_post(address, args, &block) if block_given? response = RestClient.post "#{host}/#{address}", args, {:content_type => "multipart/form-data"} do |response, request, result, &other_block| block.call(response,request,result, &other_block) end else response = RestClient.post "#{host}/#{address}", args end response.body end def is_windows? (RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/) end def is_macosx? (RbConfig::CONFIG['host_os'] =~ /darwin/) end def validate_ipa(ipa) result = false dir = Dir.mktmpdir #do |dir| unzip_file(ipa,dir) unless File.directory?("#{dir}/Payload") #macos only abort do puts "Unzipping #{ipa} to #{dir} failed: Did not find a Payload directory (invalid .ipa)." end end app_dir = Dir.foreach("#{dir}/Payload").find {|d| /\.app$/.match(d)} app = app_dir.split(".")[0] res = `otool "#{File.expand_path(dir)}/Payload/#{app_dir}/#{app}" -o 2> /dev/null | grep CalabashServer` if /CalabashServer/.match(res) puts "ipa: #{ipa} *contains* calabash.framework" result = :calabash end unless result res = `otool "#{File.expand_path(dir)}/Payload/#{app_dir}/#{app}" -o 2> /dev/null | grep FrankServer` if /FrankServer/.match(res) puts "ipa: #{ipa} *contains* FrankServer" result = :frank else puts "ipa: #{ipa} *does not contain* calabash.framework" result = false end end #end result end def self.log(message) puts "#{Time.now } #{message}" $stdout.flush end def self.log_header(message) if is_windows? puts "\n### #{message} ###" else puts "\n\e[#{35}m### #{message} ###\e[0m" end end def usage "Usage: lesspainful ? ?" end def verify_arguments server = nil log_and_abort(usage) unless app log_and_abort(usage) unless api_key abort_unless(File.exist?(app)) do puts usage puts "No such file: #{app}" end abort_unless(/\.(apk|ipa)$/ =~ app) do puts usage puts " should be either an ipa or apk file" end if is_ios? and is_macosx? and not ENV['CHECK_IPA'] == '0' log_header("Checking ipa for linking with Calabash or Frank") server = validate_ipa(app) abort_unless(server) do puts "The .ipa file does not seem to be linked with Calabash." puts "Verify that your app is linked correctly." puts "To disable this check run with Environment Variable CHECK_IPA=0" end end server end def abort(&block) yield block exit 1 end def abort_unless(condition, &block) unless condition yield block exit 1 end end def log_and_abort(message) abort do puts message end end def self.verify_files if is_android? abort_unless(File.exist?(test_server_path)) do puts "No test server found. Please run:" puts " calabash-android build #{app}" end calabash_gem = Dir.glob("vendor/cache/calabash-android-*").first abort_unless(calabash_gem) do puts "calabash-android was not packaged correct." puts "Please tell contact@lesspainful.com about this bug." end end end start_at = Time.now server = verify_arguments log_header("Checking for Gemfile") unless File.exist?("Gemfile") log("Gemfile missing.") if is_android? log("Creating Gemfile for Android") tgt = File.join(File.dirname(__FILE__),"..","lib","GemfileAndroid") elsif is_ios? log("Creating Gemfile for iOS") gemfile = "GemfileIOS" if server == :frank gemfile = "GemfileIOSFrank" end tgt = File.join(File.dirname(__FILE__),"..","lib",gemfile) else abort do puts usage puts "Your app (second argument) must be an ipa or apk file." end end FileUtils.cp(File.expand_path(tgt), "Gemfile") end log_header("Packaging") log_and_abort "Bundler failed. Please check command: bundle package" unless system("bundle package --all") log_header("Collecting files") collected_files = all_files file_paths = collected_files[:files] feature_prefix = collected_files[:feature_prefix] workspace_prefix = collected_files[:workspace_prefix] hashes = file_paths.collect { |f| digest(f) } hashes << digest(app) log_header("Verifying files") verify_files log_header("Negotiating upload") response = http_post("check_hash", {"hashes" => hashes}) cache_status = JSON.parse(response) curl_args = [] files = [] paths = [] file_paths.each do |file| if cache_status[digest(file)] #Server already knows about this file. No need to upload it. files << digest(file) else #Upload file files << File.new(file) end if file.start_with?(feature_prefix) prefix = feature_prefix else prefix = workspace_prefix end paths << file.sub(prefix, "") end app_file = cache_status[digest(app)] ? digest(app) : File.new(app) log_header("Uploading negotiated files") response = http_post("upload", {"files" => files, "paths" => paths, "app" => app_file, "api_key" => api_key, "app_filename" => File.basename(app)}) do |response, request, result, &block| case response.code when 200 response when 403 abort do puts "Invalid API key" end when 413 abort do puts "Files too large" end else abort do log "Unexpected Error. Please contact contact@lesspainful.com and provide the timestamp on your left." end end end end_at = Time.now log_header("Done (took %.1fs)" % (end_at - start_at)) log response