shenzhen_fir 0.14.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +68 -0
- data/LICENSE +19 -0
- data/README.md +198 -0
- data/Rakefile +9 -0
- data/bin/ipa +25 -0
- data/compare_varnish.jpg +0 -0
- data/lib/shenzhen/agvtool.rb +17 -0
- data/lib/shenzhen/commands/build.rb +208 -0
- data/lib/shenzhen/commands/distribute.rb +30 -0
- data/lib/shenzhen/commands/info.rb +94 -0
- data/lib/shenzhen/commands.rb +16 -0
- data/lib/shenzhen/plistbuddy.rb +9 -0
- data/lib/shenzhen/plugins/crashlytics.rb +82 -0
- data/lib/shenzhen/plugins/deploygate.rb +97 -0
- data/lib/shenzhen/plugins/fir.rb +151 -0
- data/lib/shenzhen/plugins/ftp.rb +181 -0
- data/lib/shenzhen/plugins/hockeyapp.rb +119 -0
- data/lib/shenzhen/plugins/itunesconnect.rb +142 -0
- data/lib/shenzhen/plugins/pgyer.rb +135 -0
- data/lib/shenzhen/plugins/rivierabuild.rb +81 -0
- data/lib/shenzhen/plugins/s3.rb +139 -0
- data/lib/shenzhen/plugins/testfairy.rb +99 -0
- data/lib/shenzhen/version.rb +3 -0
- data/lib/shenzhen/xcodebuild.rb +99 -0
- data/lib/shenzhen.rb +4 -0
- data/npm-debug.log +84 -0
- data/shenzhen.gemspec +37 -0
- metadata +268 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'plist'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'zip'
|
4
|
+
require 'zip/filesystem'
|
5
|
+
|
6
|
+
command :info do |c|
|
7
|
+
c.syntax = 'ipa info [options]'
|
8
|
+
c.summary = 'Show mobile provisioning information about an .ipa file'
|
9
|
+
c.description = ''
|
10
|
+
|
11
|
+
c.action do |args, options|
|
12
|
+
say_error "`security` command not found in $PATH" and abort if `which security` == ""
|
13
|
+
say_error "`codesign` command not found in $PATH" and abort if `which codesign` == ""
|
14
|
+
|
15
|
+
determine_file! unless @file = args.pop
|
16
|
+
say_error "Missing or unspecified .ipa file" and abort unless @file and ::File.exist?(@file)
|
17
|
+
|
18
|
+
Zip::File.open(@file) do |zipfile|
|
19
|
+
app_entry = zipfile.find_entry("Payload/#{File.basename(@file, File.extname(@file))}.app")
|
20
|
+
provisioning_profile_entry = zipfile.find_entry("#{app_entry.name}embedded.mobileprovision") if app_entry
|
21
|
+
|
22
|
+
if (!provisioning_profile_entry)
|
23
|
+
zipfile.dir.entries("Payload").each do |dir_entry|
|
24
|
+
if dir_entry =~ /.app$/
|
25
|
+
say "Using .app: #{dir_entry}"
|
26
|
+
app_entry = zipfile.find_entry("Payload/#{dir_entry}")
|
27
|
+
provisioning_profile_entry = zipfile.find_entry("#{app_entry.name}embedded.mobileprovision") if app_entry
|
28
|
+
break
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
say_error "Embedded mobile provisioning file not found in #{@file}" and abort unless provisioning_profile_entry
|
34
|
+
|
35
|
+
tempdir = ::File.new(Dir.mktmpdir)
|
36
|
+
begin
|
37
|
+
zipfile.each do |zip_entry|
|
38
|
+
temp_entry_path = ::File.join(tempdir.path, zip_entry.name)
|
39
|
+
|
40
|
+
FileUtils.mkdir_p(::File.dirname(temp_entry_path))
|
41
|
+
zipfile.extract(zip_entry, temp_entry_path) unless ::File.exist?(temp_entry_path)
|
42
|
+
end
|
43
|
+
|
44
|
+
temp_provisioning_profile = ::File.new(::File.join(tempdir.path, provisioning_profile_entry.name))
|
45
|
+
temp_app_directory = ::File.new(::File.join(tempdir.path, app_entry.name))
|
46
|
+
|
47
|
+
plist = Plist::parse_xml(`security cms -D -i #{temp_provisioning_profile.path}`)
|
48
|
+
|
49
|
+
codesign = `codesign -dv "#{temp_app_directory.path}" 2>&1`
|
50
|
+
codesigned = /Signed Time/ === codesign
|
51
|
+
|
52
|
+
table = Terminal::Table.new do |t|
|
53
|
+
plist.each do |key, value|
|
54
|
+
next if key == "DeveloperCertificates"
|
55
|
+
|
56
|
+
columns = []
|
57
|
+
columns << key
|
58
|
+
columns << case value
|
59
|
+
when Hash
|
60
|
+
value.collect{|k, v| "#{k}: #{v}"}.join("\n")
|
61
|
+
when Array
|
62
|
+
value.join("\n")
|
63
|
+
else
|
64
|
+
value.to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
t << columns
|
68
|
+
end
|
69
|
+
|
70
|
+
t << ["Codesigned", codesigned.to_s.capitalize]
|
71
|
+
end
|
72
|
+
|
73
|
+
puts table
|
74
|
+
|
75
|
+
rescue => e
|
76
|
+
say_error e.message
|
77
|
+
ensure
|
78
|
+
FileUtils.remove_entry_secure tempdir
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def determine_file!
|
86
|
+
files = Dir['*.ipa']
|
87
|
+
@file ||= case files.length
|
88
|
+
when 0 then nil
|
89
|
+
when 1 then files.first
|
90
|
+
else
|
91
|
+
@file = choose "Select an .ipa File:", *files
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
$:.push File.expand_path('../', __FILE__)
|
2
|
+
|
3
|
+
require 'plugins/rivierabuild'
|
4
|
+
require 'plugins/hockeyapp'
|
5
|
+
require 'plugins/testfairy'
|
6
|
+
require 'plugins/deploygate'
|
7
|
+
require 'plugins/itunesconnect'
|
8
|
+
require 'plugins/ftp'
|
9
|
+
require 'plugins/s3'
|
10
|
+
require 'plugins/crashlytics'
|
11
|
+
require 'plugins/fir'
|
12
|
+
require 'plugins/pgyer'
|
13
|
+
|
14
|
+
require 'commands/build'
|
15
|
+
require 'commands/distribute'
|
16
|
+
require 'commands/info'
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Shenzhen::Plugins
|
4
|
+
module Crashlytics
|
5
|
+
class Client
|
6
|
+
|
7
|
+
def initialize(crashlytics_path, api_token, build_secret)
|
8
|
+
@api_token, @build_secret = api_token, build_secret
|
9
|
+
|
10
|
+
@crashlytics_path = Pathname.new("#{crashlytics_path}/submit").cleanpath.to_s
|
11
|
+
say_error "Path to Crashlytics.framework/submit is invalid" and abort unless File.exists?(@crashlytics_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def upload_build(ipa, options)
|
15
|
+
command = "#{@crashlytics_path} #{@api_token} #{@build_secret} -ipaPath '#{options[:file]}'"
|
16
|
+
command += " -notesPath '#{options[:notes]}'" if options[:notes]
|
17
|
+
command += " -emails #{options[:emails]}" if options[:emails]
|
18
|
+
command += " -groupAliases #{options[:groups]}" if options[:groups]
|
19
|
+
command += " -notifications #{options[:notifications] ? 'YES' : 'NO'}"
|
20
|
+
|
21
|
+
system command
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
command :'distribute:crashlytics' do |c|
|
28
|
+
c.syntax = "ipa distribute:crashlytics [options]"
|
29
|
+
c.summary = "Distribute an .ipa file over Crashlytics"
|
30
|
+
c.description = ""
|
31
|
+
c.option '-c', '--crashlytics_path PATH', "/path/to/Crashlytics.framework/"
|
32
|
+
c.option '-f', '--file FILE', ".ipa file for the build"
|
33
|
+
c.option '-a', '--api_token TOKEN', "API Token. Available at https://www.crashlytics.com/settings/organizations"
|
34
|
+
c.option '-s', '--build_secret SECRET', "Build Secret. Available at https://www.crashlytics.com/settings/organizations"
|
35
|
+
c.option '-m', '--notes PATH', "Path to release notes file"
|
36
|
+
c.option '-e', '--emails EMAIL1,EMAIL2', "Emails of users for access"
|
37
|
+
c.option '-g', '--groups GROUPS', "Groups for users for access"
|
38
|
+
c.option '-n', '--notifications [YES | NO]', "Should send notification email to testers?"
|
39
|
+
|
40
|
+
c.action do |args, options|
|
41
|
+
determine_file! unless @file = options.file
|
42
|
+
say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
|
43
|
+
|
44
|
+
determine_crashlytics_path! unless @crashlytics_path = options.crashlytics_path || ENV['CRASHLYTICS_FRAMEWORK_PATH']
|
45
|
+
say_error "Missing path to Crashlytics.framework" and abort unless @crashlytics_path
|
46
|
+
|
47
|
+
determine_crashlytics_api_token! unless @api_token = options.api_token || ENV['CRASHLYTICS_API_TOKEN']
|
48
|
+
say_error "Missing API Token" and abort unless @api_token
|
49
|
+
|
50
|
+
determine_crashlytics_build_secret! unless @build_secret = options.build_secret || ENV['CRASHLYTICS_BUILD_SECRET']
|
51
|
+
say_error "Missing Build Secret" and abort unless @build_secret
|
52
|
+
|
53
|
+
parameters = {}
|
54
|
+
parameters[:file] = @file
|
55
|
+
parameters[:notes] = options.notes if options.notes
|
56
|
+
parameters[:emails] = options.emails if options.emails
|
57
|
+
parameters[:groups] = options.groups if options.groups
|
58
|
+
parameters[:notifications] = options.notifications == 'YES' if options.notifications
|
59
|
+
|
60
|
+
client = Shenzhen::Plugins::Crashlytics::Client.new(@crashlytics_path, @api_token, @build_secret)
|
61
|
+
|
62
|
+
if client.upload_build(@file, parameters)
|
63
|
+
say_ok "Build successfully uploaded to Crashlytics"
|
64
|
+
else
|
65
|
+
say_error "Error uploading to Crashlytics" and abort
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def determine_crashlytics_path!
|
72
|
+
@crashlytics_path ||= ask "Path to Crashlytics.framework:"
|
73
|
+
end
|
74
|
+
|
75
|
+
def determine_crashlytics_api_token!
|
76
|
+
@api_token ||= ask "API Token:"
|
77
|
+
end
|
78
|
+
|
79
|
+
def determine_crashlytics_build_secret!
|
80
|
+
@build_secret ||= ask "Build Secret:"
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'openssl'
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday_middleware'
|
5
|
+
|
6
|
+
module Shenzhen::Plugins
|
7
|
+
module DeployGate
|
8
|
+
class Client
|
9
|
+
HOSTNAME = 'deploygate.com'
|
10
|
+
|
11
|
+
def initialize(api_token, user_name)
|
12
|
+
@api_token, @user_name = api_token, user_name
|
13
|
+
@connection = Faraday.new(:url => "https://#{HOSTNAME}", :request => { :timeout => 120 }) do |builder|
|
14
|
+
builder.request :multipart
|
15
|
+
builder.request :json
|
16
|
+
builder.response :json, :content_type => /\bjson$/
|
17
|
+
builder.use FaradayMiddleware::FollowRedirects
|
18
|
+
builder.adapter :net_http
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def upload_build(ipa, options)
|
23
|
+
options.update({
|
24
|
+
:token => @api_token,
|
25
|
+
:file => Faraday::UploadIO.new(ipa, 'application/octet-stream'),
|
26
|
+
:message => options[:message] || ''
|
27
|
+
})
|
28
|
+
|
29
|
+
@connection.post("/api/users/#{@user_name}/apps", options).on_complete do |env|
|
30
|
+
yield env[:status], env[:body] if block_given?
|
31
|
+
end
|
32
|
+
|
33
|
+
rescue Faraday::Error::TimeoutError
|
34
|
+
say_error "Timed out while uploading build. Check https://deploygate.com/ to see if the upload was completed." and abort
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
command :'distribute:deploygate' do |c|
|
41
|
+
c.syntax = "ipa distribute:deploygate [options]"
|
42
|
+
c.summary = "Distribute an .ipa file over deploygate"
|
43
|
+
c.description = ""
|
44
|
+
c.option '-f', '--file FILE', ".ipa file for the build"
|
45
|
+
c.option '-a', '--api_token TOKEN', "API Token. Available at https://deploygate.com/settings"
|
46
|
+
c.option '-u', '--user_name USER_NAME', "User Name. Available at https://deploygate.com/settings"
|
47
|
+
c.option '-m', '--message MESSAGE', "Release message for the build"
|
48
|
+
c.option '-d', '--distribution_key DESTRIBUTION_KEY', "distribution key for distribution page"
|
49
|
+
c.option '-n', '--disable_notify', "disable notification"
|
50
|
+
c.option '-r', '--release_note RELEASE_NOTE', "release note for distribution page"
|
51
|
+
c.option '-v', '--visibility (private|public)', "privacy setting ( require public for personal free account)"
|
52
|
+
|
53
|
+
c.action do |args, options|
|
54
|
+
determine_file! unless @file = options.file
|
55
|
+
say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
|
56
|
+
|
57
|
+
determine_deploygate_api_token! unless @api_token = options.api_token || ENV['DEPLOYGATE_API_TOKEN']
|
58
|
+
say_error "Missing API Token" and abort unless @api_token
|
59
|
+
|
60
|
+
determine_deploygate_user_name! unless @user_name = options.user_name || ENV['DEPLOYGATE_USER_NAME']
|
61
|
+
say_error "Missing User Name" and abort unless @api_token
|
62
|
+
|
63
|
+
@message = options.message
|
64
|
+
@distribution_key = options.distribution_key || ENV['DEPLOYGATE_DESTRIBUTION_KEY']
|
65
|
+
@release_note = options.release_note
|
66
|
+
@disable_notify = ! options.disable_notify.nil? ? "yes" : nil
|
67
|
+
@visibility = options.visibility
|
68
|
+
@message = options.message
|
69
|
+
|
70
|
+
parameters = {}
|
71
|
+
parameters[:file] = @file
|
72
|
+
parameters[:message] = @message
|
73
|
+
parameters[:distribution_key] = @distribution_key if @distribution_key
|
74
|
+
parameters[:release_note] = @release_note if @release_note
|
75
|
+
parameters[:disable_notify] = @disable_notify if @disable_notify
|
76
|
+
parameters[:visibility] = @visibility if @visibility
|
77
|
+
parameters[:replace] = "true" if options.replace
|
78
|
+
|
79
|
+
client = Shenzhen::Plugins::DeployGate::Client.new(@api_token, @user_name)
|
80
|
+
response = client.upload_build(@file, parameters)
|
81
|
+
if (200...300) === response.status and not response.body["error"]
|
82
|
+
say_ok "Build successfully uploaded to DeployGate"
|
83
|
+
else
|
84
|
+
say_error "Error uploading to DeployGate: #{response.body["error"] || "(Unknown Error)"}" and abort
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def determine_deploygate_api_token!
|
91
|
+
@api_token ||= ask "API Token:"
|
92
|
+
end
|
93
|
+
|
94
|
+
def determine_deploygate_user_name!
|
95
|
+
@user_name ||= ask "User Name:"
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'openssl'
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday_middleware'
|
5
|
+
|
6
|
+
module Shenzhen::Plugins
|
7
|
+
module Fir
|
8
|
+
class Client
|
9
|
+
HOSTNAME = 'api.fir.im'
|
10
|
+
VERSION = 'v2'
|
11
|
+
|
12
|
+
|
13
|
+
def initialize(user_token)
|
14
|
+
@user_token = user_token
|
15
|
+
|
16
|
+
@connection = Faraday.new(:url => "http://#{HOSTNAME}") do |builder|
|
17
|
+
builder.request :url_encoded
|
18
|
+
builder.response :json
|
19
|
+
builder.use FaradayMiddleware::FollowRedirects
|
20
|
+
builder.adapter :net_http
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
#
|
26
|
+
#get upload ticket
|
27
|
+
#
|
28
|
+
def get_upload_ticket bundle_id
|
29
|
+
options = {
|
30
|
+
:type => 'ios',
|
31
|
+
:bundle_id => bundle_id,
|
32
|
+
:api_token => @user_token
|
33
|
+
}
|
34
|
+
|
35
|
+
response = @connection.post('/apps', options) do |env|
|
36
|
+
yield env[:status], env[:body] if block_given?
|
37
|
+
end
|
38
|
+
|
39
|
+
rescue Faraday::Error::TimeoutError
|
40
|
+
say_error "Timed out while geting upload ticket." and abort
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
#upload file
|
45
|
+
#
|
46
|
+
def upload_file_and_update_app_info ipa, options
|
47
|
+
connection = Faraday.new(:url => options['upload_url'], :request => { :timeout => 360 }) do |builder|
|
48
|
+
builder.request :multipart
|
49
|
+
builder.response :json
|
50
|
+
builder.use FaradayMiddleware::FollowRedirects
|
51
|
+
builder.adapter :net_http
|
52
|
+
end
|
53
|
+
|
54
|
+
form_options = {
|
55
|
+
:key => options['key'],
|
56
|
+
:token => options['token'],
|
57
|
+
:file => Faraday::UploadIO.new(ipa, 'application/octet-stream'),
|
58
|
+
"x:name" => options[:name],
|
59
|
+
"x:version" => options[:version],
|
60
|
+
"x:build" => options[:build],
|
61
|
+
"x:release_type" => options[:release_type],
|
62
|
+
"x:changelog" => options[:changelog]
|
63
|
+
}
|
64
|
+
p "=================uploading====================="
|
65
|
+
connection.post('/', form_options).on_complete do |env|
|
66
|
+
yield env[:status], env[:body] if block_given?
|
67
|
+
end
|
68
|
+
rescue Errno::EPIPE
|
69
|
+
say_error "Upload failed. Check internet connection is ok." and abort
|
70
|
+
rescue Faraday::Error::TimeoutError
|
71
|
+
say_error "Timed out while uploading build. Check https://fir.im// to see if the upload was completed." and abort
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
command :'distribute:fir' do |c|
|
78
|
+
c.syntax = "ipa distribute:fir [options]"
|
79
|
+
c.summary = "请使用新版api_token => http://fir.im/user/info 获取 \n Distribute an .ipa file over fir.im"
|
80
|
+
c.description = ""
|
81
|
+
c.option '-f', '--file FILE', ".ipa file for the build"
|
82
|
+
c.option '-u', '--user_token 即fir.im 的 api_token ', "fir.im 的 api_token 在 http://fir.im/user/info 获取"
|
83
|
+
c.option '-a', '--app_id APPID', "App Id (iOS Bundle identifier)"
|
84
|
+
c.option '-n', '--notes NOTES', "Release notes for the build"
|
85
|
+
c.option '-N', '--app_name APP_NAME', "the name for app"
|
86
|
+
c.option '-R', '--release_type RELEASE_TYPE', "release_type for app default adhoc"
|
87
|
+
c.option '-V', '--app_version VERSION', "应用编译号 build"
|
88
|
+
c.option '-S', '--short_version SHORT', "App Short Version"
|
89
|
+
|
90
|
+
c.action do |args, options|
|
91
|
+
determine_file! unless @file = options.file
|
92
|
+
say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
|
93
|
+
|
94
|
+
determine_fir_user_token! unless @user_token = options.user_token || ENV['FIR_USER_TOKEN']
|
95
|
+
say_error "Missing User Token" and abort unless @user_token
|
96
|
+
determine_fir_app_id! unless @app_id = options.app_id || ENV['FIR_APP_ID']
|
97
|
+
say_error "Missing App Id" and abort unless @app_id
|
98
|
+
|
99
|
+
determine_notes! unless @notes = options.notes
|
100
|
+
say_error "Missing release notes" and abort unless @notes
|
101
|
+
|
102
|
+
determine_app_version! unless @app_version = options.app_version
|
103
|
+
|
104
|
+
determine_short_version! unless @short_version = options.short_version
|
105
|
+
|
106
|
+
client = Shenzhen::Plugins::Fir::Client.new(@user_token)
|
107
|
+
#get upload ticket
|
108
|
+
app_response = client.get_upload_ticket(@app_id)
|
109
|
+
if app_response.status == 201
|
110
|
+
|
111
|
+
upload_app_options = app_response.body['cert']['binary']
|
112
|
+
app_short_uri = app_response.body['short']
|
113
|
+
if options.app_name || ENV['APP_NAME']
|
114
|
+
upload_app_options[:name] = options.app_name || ENV['APP_NAME']
|
115
|
+
end
|
116
|
+
upload_app_options[:release_type] = options.release_type || "adhoc"
|
117
|
+
upload_app_options[:version] = @short_version
|
118
|
+
upload_app_options[:build] = @app_version
|
119
|
+
upload_app_options[:changelog] = @notes
|
120
|
+
|
121
|
+
#upload file
|
122
|
+
upload_response = client.upload_file_and_update_app_info(@file, upload_app_options)
|
123
|
+
|
124
|
+
if upload_response.status == 200
|
125
|
+
say_ok "Build successfully uploaded to Fir, visit url: http://fir.im/#{app_short_uri}"
|
126
|
+
else
|
127
|
+
say_error "Error uploading to Fir: #{upload_response.body[:error]}" and abort
|
128
|
+
end
|
129
|
+
else
|
130
|
+
say_error "Error getting app information: #{app_response.body[:error]}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def determine_fir_user_token!
|
137
|
+
@user_token ||= ask "User Token:"
|
138
|
+
end
|
139
|
+
|
140
|
+
def determine_fir_app_id!
|
141
|
+
@app_id ||= ask "App Id:"
|
142
|
+
end
|
143
|
+
|
144
|
+
def determine_app_version!
|
145
|
+
@app_version ||= ask "App Version:"
|
146
|
+
end
|
147
|
+
|
148
|
+
def determine_short_version!
|
149
|
+
@short_version ||= ask "Short Version:"
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'net/ftp'
|
2
|
+
require 'net/sftp'
|
3
|
+
|
4
|
+
module Shenzhen::Plugins
|
5
|
+
module FTP
|
6
|
+
class Client
|
7
|
+
|
8
|
+
def initialize(host, port, user, password)
|
9
|
+
@host, @port, @user, @password = host, port, user, password
|
10
|
+
end
|
11
|
+
|
12
|
+
def upload(ipa, options = {})
|
13
|
+
connection = Net::FTP.new
|
14
|
+
connection.passive = true
|
15
|
+
connection.connect(@host, @port)
|
16
|
+
|
17
|
+
path = expand_path_with_substitutions_from_ipa_plist(ipa, options[:path])
|
18
|
+
|
19
|
+
begin
|
20
|
+
connection.login(@user, @password) rescue raise "Login authentication failed"
|
21
|
+
|
22
|
+
if options[:mkdir]
|
23
|
+
components, pwd = path.split(/\//).reject(&:empty?), nil
|
24
|
+
components.each do |component|
|
25
|
+
pwd = File.join(*[pwd, component].compact)
|
26
|
+
|
27
|
+
begin
|
28
|
+
connection.mkdir pwd
|
29
|
+
rescue => exception
|
30
|
+
raise exception unless /File exists/ === exception.message
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
connection.chdir path unless path.empty?
|
36
|
+
connection.putbinaryfile ipa, File.basename(ipa)
|
37
|
+
connection.putbinaryfile(options[:dsym], File.basename(options[:dsym])) if options[:dsym]
|
38
|
+
ensure
|
39
|
+
connection.close
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def expand_path_with_substitutions_from_ipa_plist(ipa, path)
|
46
|
+
substitutions = path.scan(/\{CFBundle[^}]+\}/)
|
47
|
+
return path if substitutions.empty?
|
48
|
+
|
49
|
+
Dir.mktmpdir do |dir|
|
50
|
+
system "unzip -q #{ipa} -d #{dir} 2> /dev/null"
|
51
|
+
|
52
|
+
plist = Dir["#{dir}/**/*.app/Info.plist"].last
|
53
|
+
|
54
|
+
substitutions.uniq.each do |substitution|
|
55
|
+
key = substitution[1...-1]
|
56
|
+
value = Shenzhen::PlistBuddy.print(plist, key)
|
57
|
+
|
58
|
+
path.gsub!(Regexp.new(substitution), value) if value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
return path
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module SFTP
|
68
|
+
class Client < Shenzhen::Plugins::FTP::Client
|
69
|
+
def upload(ipa, options = {})
|
70
|
+
session = Net::SSH.start(@host, @user, :password => @password, :port => @port)
|
71
|
+
connection = Net::SFTP::Session.new(session).connect!
|
72
|
+
|
73
|
+
path = expand_path_with_substitutions_from_ipa_plist(ipa, options[:path])
|
74
|
+
|
75
|
+
begin
|
76
|
+
connection.stat!(path) do |response|
|
77
|
+
connection.mkdir! path if options[:mkdir] and not response.ok?
|
78
|
+
|
79
|
+
connection.upload! ipa, determine_file_path(File.basename(ipa), path)
|
80
|
+
connection.upload! options[:dsym], determine_file_path(File.basename(options[:dsym]), path) if options[:dsym]
|
81
|
+
end
|
82
|
+
ensure
|
83
|
+
connection.close_channel
|
84
|
+
session.shutdown!
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def determine_file_path(file_name, path)
|
89
|
+
if path.empty?
|
90
|
+
file_name
|
91
|
+
else
|
92
|
+
"#{path}/#{file_name}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
command :'distribute:ftp' do |c|
|
100
|
+
c.syntax = "ipa distribute:ftp [options]"
|
101
|
+
c.summary = "Distribute an .ipa file over FTP"
|
102
|
+
c.description = ""
|
103
|
+
|
104
|
+
c.example '', '$ ipa distribute:ftp --host 127.0.0.1 -f ./file.ipa -u username --path "/path/to/folder/{CFBundleVersion}/" --mkdir'
|
105
|
+
|
106
|
+
c.option '-f', '--file FILE', ".ipa file for the build"
|
107
|
+
c.option '-d', '--dsym FILE', "zipped .dsym package for the build"
|
108
|
+
c.option '-h', '--host HOST', "FTP host"
|
109
|
+
c.option '-u', '--user USER', "FTP user"
|
110
|
+
c.option '-p', '--password PASS', "FTP password"
|
111
|
+
c.option '-P', '--path PATH', "FTP path. Values from Info.plist will be substituted for keys wrapped in {} \n\t\t e.g. \"/path/to/folder/{CFBundleVersion}/\" would be evaluated as \"/path/to/folder/1.0.0/\""
|
112
|
+
c.option '--port PORT', "FTP port"
|
113
|
+
c.option '--protocol [PROTOCOL]', [:ftp, :sftp], "Protocol to use (ftp, sftp)"
|
114
|
+
c.option '--[no-]mkdir', "Create directories on FTP if they don't already exist"
|
115
|
+
|
116
|
+
c.action do |args, options|
|
117
|
+
options.default :mkdir => true
|
118
|
+
|
119
|
+
determine_file! unless @file = options.file
|
120
|
+
say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
|
121
|
+
|
122
|
+
determine_dsym! unless @dsym = options.dsym
|
123
|
+
say_warning "Specified dSYM.zip file doesn't exist" unless @dsym and File.exist?(@dsym)
|
124
|
+
|
125
|
+
determine_host! unless @host = options.host
|
126
|
+
say_error "Missing FTP host" and abort unless @host
|
127
|
+
|
128
|
+
determine_port!(options.protocol) unless @port = options.port
|
129
|
+
|
130
|
+
determine_user! unless @user = options.user
|
131
|
+
say_error "Missing FTP user" and abort unless @user
|
132
|
+
|
133
|
+
@password = options.password
|
134
|
+
if !@password && options.protocol != :sftp
|
135
|
+
determine_password!
|
136
|
+
say_error "Missing FTP password" and abort unless @password
|
137
|
+
end
|
138
|
+
|
139
|
+
@path = options.path || ""
|
140
|
+
|
141
|
+
client = case options.protocol
|
142
|
+
when :sftp
|
143
|
+
Shenzhen::Plugins::SFTP::Client.new(@host, @port, @user, @password)
|
144
|
+
else
|
145
|
+
Shenzhen::Plugins::FTP::Client.new(@host, @port, @user, @password)
|
146
|
+
end
|
147
|
+
|
148
|
+
begin
|
149
|
+
client.upload @file, {:path => @path, :dsym => @dsym, :mkdir => !!options.mkdir}
|
150
|
+
say_ok "Build successfully uploaded to FTP"
|
151
|
+
rescue => exception
|
152
|
+
say_error "Error while uploading to FTP: #{exception}"
|
153
|
+
raise if options.trace
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def determine_host!
|
160
|
+
@host ||= ask "FTP Host:"
|
161
|
+
end
|
162
|
+
|
163
|
+
def determine_port!(protocol)
|
164
|
+
@port = case protocol
|
165
|
+
when :sftp
|
166
|
+
Net::SSH::Transport::Session::DEFAULT_PORT
|
167
|
+
else
|
168
|
+
Net::FTP::FTP_PORT
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def determine_user!
|
173
|
+
@user ||= ask "Username:"
|
174
|
+
end
|
175
|
+
|
176
|
+
def determine_password!
|
177
|
+
@password ||= password "Password:"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
alias_command :'distribute:sftp', :'distribute:ftp', '--protocol', 'sftp'
|