shenzhen_fir 0.14.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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'
|