appsendr 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest +0 -0
- data/README.rdoc +50 -0
- data/Rakefile +16 -0
- data/appsendr.gemspec +41 -0
- data/bin/appsendr +14 -0
- data/lib/appsendr/binary_plist.rb +514 -0
- data/lib/appsendr/client.rb +115 -0
- data/lib/appsendr/command.rb +68 -0
- data/lib/appsendr/commands/app.rb +167 -0
- data/lib/appsendr/commands/auth.rb +155 -0
- data/lib/appsendr/commands/base.rb +63 -0
- data/lib/appsendr/commands/build.rb +198 -0
- data/lib/appsendr/commands/deploy.rb +41 -0
- data/lib/appsendr/commands/help.rb +87 -0
- data/lib/appsendr/commands/testers.rb +115 -0
- data/lib/appsendr/commands/version.rb +7 -0
- data/lib/appsendr/constants.rb +5 -0
- data/lib/appsendr/helpers.rb +120 -0
- data/lib/appsendr.rb +3 -0
- metadata +151 -0
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'zip/zip'
|
2
|
+
require 'ftools'
|
3
|
+
module AppSendr::Command
|
4
|
+
class Build < Base
|
5
|
+
attr_accessor :app_path, :app_dir, :app_name, :ipa_path, :provisioning_name, :provisioning_path, :configuration, :project_info
|
6
|
+
|
7
|
+
|
8
|
+
def index
|
9
|
+
if require_project(1,"build","your active configuration name")
|
10
|
+
@configuration = load_configuration
|
11
|
+
display "=== Building Xcode project with configuration #{@configuration}"
|
12
|
+
compile(@configuration)
|
13
|
+
|
14
|
+
@app_dir = path_to_app_dir
|
15
|
+
@app_path = @app_dir+"/#{app_name}"
|
16
|
+
@ipa_path = build_ipa_with_app_at_path
|
17
|
+
info = app_info
|
18
|
+
bundle_identifier ||= info['CFBundleIdentifier']
|
19
|
+
icon_path = icon_path(info)
|
20
|
+
appsendr.upload(read_app_id,@ipa_path, profile_path,nil,bundle_identifier,icon_path)
|
21
|
+
FileUtils.rm_rf @ipa_path
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def clean
|
28
|
+
require_project_dir("clean")
|
29
|
+
require_project_droppr
|
30
|
+
config = args.join(" ").strip
|
31
|
+
if !config.empty?
|
32
|
+
config = "-configuration #{config} "
|
33
|
+
end
|
34
|
+
%x[xcodebuild #{config} clean]
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def compile(build_style, ossdk=nil, target=nil)
|
40
|
+
iossdk = "iphoneos4.1"
|
41
|
+
iossdk ||= ossdk
|
42
|
+
sdk_str = "-sdk "+iossdk
|
43
|
+
|
44
|
+
if target
|
45
|
+
target_str = "-target "+target
|
46
|
+
end
|
47
|
+
xcode = "xcodebuild #{sdk_str} #{target_str}-configuration #{build_style} > /dev/null 2>&1"
|
48
|
+
system(xcode)
|
49
|
+
end
|
50
|
+
|
51
|
+
def build_ipa_with_app_at_path
|
52
|
+
payload_path = @app_dir+"/Payload"
|
53
|
+
ipa_path = @app_dir+"/#{ipa_app_name(app_name)}"
|
54
|
+
|
55
|
+
|
56
|
+
FileUtils.rm_rf payload_path
|
57
|
+
Dir.mkdir(payload_path)
|
58
|
+
`cp -r #{@app_path.gsub(" ","\\ ")} #{payload_path.gsub(" ","\\ ")+"/"+app_name.gsub(" ","\\ ")}`
|
59
|
+
build_ipa(payload_path.gsub(" ","\\ "),ipa_path.gsub(" ","\\ "))
|
60
|
+
|
61
|
+
FileUtils.rm_rf payload_path
|
62
|
+
|
63
|
+
return ipa_path
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
def build_ipa(payload_path,ipa_path)
|
68
|
+
#ditto_exec = "ditto -c -k --keepParent -rsrc #{payload_path} #{ipa_path}"
|
69
|
+
ditto_exec = "ditto -c -k --keepParent --sequesterRsrc #{payload_path} #{ipa_path}"
|
70
|
+
#display ditto_exec
|
71
|
+
system(ditto_exec)
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
|
76
|
+
#povisioning
|
77
|
+
|
78
|
+
def profile_path
|
79
|
+
return @provisioning_path if @provisioning_path
|
80
|
+
@provisioning_path = provisioning_profiles_dir + "/#{profile_name}"
|
81
|
+
@provisioning_path
|
82
|
+
end
|
83
|
+
|
84
|
+
def provisioning_profiles_dir
|
85
|
+
File.expand_path("~/Library/MobileDevice/Provisioning\ Profiles/")
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def profile_name
|
90
|
+
return @provisioning_name if @provisioning_name
|
91
|
+
error "Build is not valid for AdHoc distribution" unless valid_build?
|
92
|
+
|
93
|
+
mem_buf = File.open(@app_path+"/embedded.mobileprovision","rb") {|io| io.read}
|
94
|
+
name = mem_buf.scan(/UUID<\/key>[\s]+<string>(.*)<\/string>/).flatten.first
|
95
|
+
@provisioning_name = name + ".mobileprovision"
|
96
|
+
@provisioning_name
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
## helpers
|
101
|
+
def load_configuration
|
102
|
+
@configuration = args.join(" ").strip
|
103
|
+
error "That configuration does not exist.\nAvailable configurations:\n#{project_info[:configurations].join("\n")}" unless valid_configuration?
|
104
|
+
@configuration
|
105
|
+
end
|
106
|
+
|
107
|
+
def valid_configuration?
|
108
|
+
return false unless @configuration
|
109
|
+
info = project_info
|
110
|
+
info[:configurations].include?(@configuration)
|
111
|
+
end
|
112
|
+
|
113
|
+
def project_info
|
114
|
+
return @project_info if @project_info
|
115
|
+
result = %x[xcodebuild -list]
|
116
|
+
project_info = result.split("\n")
|
117
|
+
name = project_info.shift.scan(/\"(.*)\"/).flatten.first
|
118
|
+
project_info.shift
|
119
|
+
project_info.pop #remove last line
|
120
|
+
project_info.pop #remove
|
121
|
+
on_targets = true
|
122
|
+
targets = []
|
123
|
+
configs = []
|
124
|
+
project_info.each{|ln|
|
125
|
+
if ln.strip == "Build Configurations:"
|
126
|
+
on_targets = false
|
127
|
+
end
|
128
|
+
if ln.strip != "Build Configurations:" and ln.strip != "Targets:" and ln.strip!=""
|
129
|
+
ln = ln.gsub("(Active)","").strip
|
130
|
+
on_targets ? targets.push(ln) : configs.push(ln)
|
131
|
+
end
|
132
|
+
}
|
133
|
+
|
134
|
+
@project_info = {:name => name, :targets=>targets, :configurations=>configs}
|
135
|
+
@project_info
|
136
|
+
end
|
137
|
+
def icon_path(info)
|
138
|
+
icon_files = info['CFBundleIconFiles']
|
139
|
+
icon_path = nil
|
140
|
+
icon_file = nil
|
141
|
+
if icon_files.nil? or icon_files.count == 0
|
142
|
+
icon_file = info['CFBundleIconFile']
|
143
|
+
else
|
144
|
+
large_icon = icon_files.grep(/@2x/i)
|
145
|
+
if large_icon.nil?
|
146
|
+
large_icon = icon_files.first
|
147
|
+
end
|
148
|
+
icon_file = large_icon
|
149
|
+
end
|
150
|
+
if icon_file and !icon_file.empty?
|
151
|
+
all_dir_contents = Dir["**/*"]
|
152
|
+
all_dir_contents = all_dir_contents - all_dir_contents.grep(/^build/)
|
153
|
+
|
154
|
+
icon = all_dir_contents.grep(%r{#{icon_file}}).flatten.first
|
155
|
+
if icon
|
156
|
+
icon_path = Dir.pwd+"/#{icon}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
return icon_path
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
def valid_build?
|
164
|
+
return unless @app_path
|
165
|
+
File.exist?(@app_path+"/embedded.mobileprovision")
|
166
|
+
end
|
167
|
+
|
168
|
+
def app_info
|
169
|
+
return unless @app_path
|
170
|
+
mem_buf = File.open(@app_path+"/Info.plist","rb") {|io| io.read}
|
171
|
+
plist = AppSendr::Plist::Binary.decode_binary_plist(mem_buf)
|
172
|
+
plist
|
173
|
+
end
|
174
|
+
|
175
|
+
def path_to_app_dir
|
176
|
+
Dir.pwd+"/build/#{@configuration}-iphoneos"
|
177
|
+
end
|
178
|
+
|
179
|
+
def app_name
|
180
|
+
if @app_dir.nil?
|
181
|
+
return unless @configuration
|
182
|
+
@app_dir = path_to_app_dir
|
183
|
+
end
|
184
|
+
return @app_name if @app_name
|
185
|
+
@app_name = Dir.entries(@app_dir).grep(/.+\.app$/).first
|
186
|
+
@app_name
|
187
|
+
end
|
188
|
+
|
189
|
+
def ipa_app_name(app_name)
|
190
|
+
app_name.gsub(/\.app$/,".ipa")
|
191
|
+
end
|
192
|
+
|
193
|
+
def project_name
|
194
|
+
return Dir.entries(Dir.pwd).grep(/.+\.xcodeproj/).first.sub("",".xcodeproj")
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module AppSendr::Command
|
4
|
+
class Deploy < Build ## Inherhits from Deploy
|
5
|
+
|
6
|
+
def index
|
7
|
+
#
|
8
|
+
# opts = OptionParser.new
|
9
|
+
# opts.on('-m', '--message') { output_version ; exit 0 }
|
10
|
+
#
|
11
|
+
# opts.parse!(args) rescue return false
|
12
|
+
#
|
13
|
+
#
|
14
|
+
@configuration = load_configuration
|
15
|
+
if require_project(1,"deploy","your active configuration name",true)
|
16
|
+
@app_dir = path_to_app_dir
|
17
|
+
@app_path = @app_dir+"/#{app_name}"
|
18
|
+
if @app_name
|
19
|
+
@notes = ask_for_notes
|
20
|
+
|
21
|
+
@ipa_path = build_ipa_with_app_at_path
|
22
|
+
info = app_info
|
23
|
+
bundle_identifier ||= info['CFBundleIdentifier']
|
24
|
+
icon_path = icon_path(info)
|
25
|
+
appsendr.upload(read_app_id,@ipa_path, profile_path,@notes,bundle_identifier,icon_path)
|
26
|
+
FileUtils.rm_rf @ipa_path
|
27
|
+
else
|
28
|
+
error "No .app file was built for the configuration \"#{@configuration}\". Compile your app or use the \"build\" command."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
def ask_for_notes
|
35
|
+
print "Release Notes: "
|
36
|
+
notes = ask
|
37
|
+
return notes.strip
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module AppSendr::Command
|
2
|
+
class Help < Base
|
3
|
+
class HelpGroup < Array
|
4
|
+
attr_reader :title
|
5
|
+
|
6
|
+
def initialize(title)
|
7
|
+
@title = title
|
8
|
+
end
|
9
|
+
|
10
|
+
def command(name, description)
|
11
|
+
self << [name, description]
|
12
|
+
end
|
13
|
+
|
14
|
+
def space
|
15
|
+
self << ['', '']
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.groups
|
20
|
+
@groups ||= []
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.group(title, &block)
|
24
|
+
groups << begin
|
25
|
+
group = HelpGroup.new(title)
|
26
|
+
yield group
|
27
|
+
group
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.create_default_groups!
|
32
|
+
group 'Commands' do |group|
|
33
|
+
group.command 'help', 'show this usage'
|
34
|
+
group.command 'version', 'show the gem version'
|
35
|
+
group.space
|
36
|
+
group.command 'list', 'list your apps'
|
37
|
+
group.command 'link', 'link your app with an exsiting one in appsendr'
|
38
|
+
group.command 'create <name>', 'create a new app'
|
39
|
+
group.space
|
40
|
+
group.command 'build <active configuration>', 'build your xcode project and deploy'
|
41
|
+
group.command 'build:clean', 'clean your xcode project'
|
42
|
+
group.space
|
43
|
+
group.command 'deploy <active configuration>', 'deploy the current build'
|
44
|
+
group.space
|
45
|
+
group.command 'testers', 'list testers'
|
46
|
+
group.command 'testers:add <email> <name>', 'add a tester'
|
47
|
+
group.command 'testers:remove <email> ', 'remove a tester'
|
48
|
+
group.command 'testers:clear', 'remove all testers'
|
49
|
+
group.command 'testers:notify', 'notify testers about the latest build'
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
def index
|
56
|
+
display usage
|
57
|
+
end
|
58
|
+
|
59
|
+
def version
|
60
|
+
display AppSendr::Client.version
|
61
|
+
end
|
62
|
+
|
63
|
+
def usage
|
64
|
+
longest_command_length = self.class.groups.map do |group|
|
65
|
+
group.map { |g| g.first.length }
|
66
|
+
end.flatten.max
|
67
|
+
|
68
|
+
self.class.groups.inject(StringIO.new) do |output, group|
|
69
|
+
output.puts "=== %s" % group.title
|
70
|
+
output.puts
|
71
|
+
|
72
|
+
group.each do |command, description|
|
73
|
+
if command.empty?
|
74
|
+
output.puts
|
75
|
+
else
|
76
|
+
output.puts "%-*s # %s" % [longest_command_length, command, description]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
output.puts
|
81
|
+
output
|
82
|
+
end.string
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
AppSendr::Command::Help.create_default_groups!
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module AppSendr::Command
|
2
|
+
class Testers < Base
|
3
|
+
def index #output testers
|
4
|
+
if in_project_dir?
|
5
|
+
unless has_project_droppr?
|
6
|
+
require_project_droppr
|
7
|
+
return
|
8
|
+
end
|
9
|
+
|
10
|
+
testers = appsendr.testers(read_app_id)
|
11
|
+
if testers["message"].size > 0
|
12
|
+
display "=== Your testers"
|
13
|
+
i = 0
|
14
|
+
display testers["message"].map {|app, id|
|
15
|
+
"#{i+=1}. #{app['tester']['name']} - #{app['tester']['email']}"
|
16
|
+
}.join("\n")
|
17
|
+
else
|
18
|
+
display "You have no testers."
|
19
|
+
end
|
20
|
+
|
21
|
+
# f = testers_file
|
22
|
+
# return unless f
|
23
|
+
# display "=== Testers"
|
24
|
+
# f.each do |line|
|
25
|
+
# tester = line.split(',')
|
26
|
+
# email = tester.first.strip
|
27
|
+
# display "#{email},#{name}"
|
28
|
+
# end
|
29
|
+
# f.close
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def add
|
34
|
+
|
35
|
+
if require_project(2,"add testers","an email and name", true)
|
36
|
+
f = testers_file_append
|
37
|
+
return unless f
|
38
|
+
email = args.shift.strip
|
39
|
+
name = args.join(" ").strip
|
40
|
+
f.puts "#{email},#{name}"
|
41
|
+
f.close
|
42
|
+
|
43
|
+
appsendr.add_tester(read_app_id,email,name)
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def remove
|
49
|
+
if require_project(1,"remove testers","an email")
|
50
|
+
entered_email = args.shift.strip
|
51
|
+
|
52
|
+
f = testers_file
|
53
|
+
return unless f
|
54
|
+
out = ""
|
55
|
+
found_email = nil
|
56
|
+
f.each do |line|
|
57
|
+
tester = line.split(',')
|
58
|
+
email = tester.first.strip
|
59
|
+
if email == entered_email
|
60
|
+
found_email = email
|
61
|
+
out << ""
|
62
|
+
else
|
63
|
+
out << line
|
64
|
+
end
|
65
|
+
end
|
66
|
+
f.pos = 0
|
67
|
+
f.print out
|
68
|
+
f.truncate(f.pos)
|
69
|
+
|
70
|
+
appsendr.remove_tester(read_app_id,entered_email)
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def clear
|
76
|
+
if require_project(0,"clear testers",nil)
|
77
|
+
f = testers_file_truncate
|
78
|
+
return unless f
|
79
|
+
f.close
|
80
|
+
|
81
|
+
appsendr.add_tester(read_app_id)
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def notify
|
87
|
+
require_project_dir("to notify testers")
|
88
|
+
require_project_droppr
|
89
|
+
appsendr.notify(read_app_id)
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
|
95
|
+
def testers
|
96
|
+
AppSendr::PROJECT_DIR+"/testers"
|
97
|
+
end
|
98
|
+
|
99
|
+
def testers_file
|
100
|
+
return nil unless in_project_dir?
|
101
|
+
return File.open(testers, File::RDWR|File::CREAT, 0600)
|
102
|
+
end
|
103
|
+
|
104
|
+
def testers_file_append
|
105
|
+
return nil unless in_project_dir?
|
106
|
+
return File.open(testers, File::WRONLY|File::APPEND|File::CREAT, 0600)
|
107
|
+
end
|
108
|
+
|
109
|
+
def testers_file_truncate
|
110
|
+
return nil unless in_project_dir?
|
111
|
+
return File.open(path, File::WRONLY|File::TRUNC|File::CREAT, 0600)
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module AppSendr
|
2
|
+
module Helpers
|
3
|
+
def home_directory
|
4
|
+
running_on_windows? ? ENV['USERPROFILE'] : ENV['HOME']
|
5
|
+
end
|
6
|
+
|
7
|
+
def running_on_windows?
|
8
|
+
RUBY_PLATFORM =~ /mswin32|mingw32/
|
9
|
+
end
|
10
|
+
|
11
|
+
def running_on_a_mac?
|
12
|
+
RUBY_PLATFORM =~ /-darwin\d/
|
13
|
+
end
|
14
|
+
|
15
|
+
def display(msg, newline=true)
|
16
|
+
if newline
|
17
|
+
puts(msg)
|
18
|
+
else
|
19
|
+
print(msg)
|
20
|
+
STDOUT.flush
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def credentials_file
|
25
|
+
"#{home_directory}/#{AppSendr::APPDROPPR_DIR}/credentials"
|
26
|
+
end
|
27
|
+
|
28
|
+
def credentials_setup?
|
29
|
+
File.exist?(credentials_file)
|
30
|
+
end
|
31
|
+
|
32
|
+
def project_appsendr
|
33
|
+
Dir.pwd+"/#{AppSendr::PROJECT_DIR}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def project_appsendr_app
|
37
|
+
project_appsendr+"/app"
|
38
|
+
end
|
39
|
+
|
40
|
+
def read_app_id
|
41
|
+
File.exists?(project_appsendr_app) and File.read(project_appsendr_app).split("\n").first
|
42
|
+
end
|
43
|
+
def read_app
|
44
|
+
File.exists?(project_appsendr_app) and File.read(project_appsendr_app).split("\n")
|
45
|
+
end
|
46
|
+
|
47
|
+
def has_project_droppr?
|
48
|
+
return false unless File.directory?(project_appsendr)
|
49
|
+
return File.exist?(project_appsendr_app)
|
50
|
+
end
|
51
|
+
|
52
|
+
def require_project_droppr
|
53
|
+
error "Project directory doesn't exist. Have you created an app droppr for this app?" unless has_project_droppr?
|
54
|
+
end
|
55
|
+
|
56
|
+
def require_project_dir(action)
|
57
|
+
error "You must be in a Xcode project directory to #{action}" unless in_project_dir?
|
58
|
+
end
|
59
|
+
|
60
|
+
def in_project_dir?
|
61
|
+
return Dir.entries(Dir.pwd).grep(/.+\.xcodeproj/).first
|
62
|
+
end
|
63
|
+
|
64
|
+
def require_project(num_args,action,wrong_args,greater_than=false)
|
65
|
+
if in_project_dir? and has_project_droppr?
|
66
|
+
if args.count >= num_args
|
67
|
+
if greater_than
|
68
|
+
return true
|
69
|
+
elsif !greater_than and (args.count == num_args)
|
70
|
+
return true
|
71
|
+
else
|
72
|
+
return false
|
73
|
+
end
|
74
|
+
elsif wrong_args
|
75
|
+
error "You must include #{wrong_args}"
|
76
|
+
end
|
77
|
+
else
|
78
|
+
require_project_dir(action)
|
79
|
+
require_project_droppr
|
80
|
+
end
|
81
|
+
|
82
|
+
return false
|
83
|
+
end
|
84
|
+
|
85
|
+
def require_in_project_and_no_droppr(num_args,action,wrong_args,greater_than=false)
|
86
|
+
if in_project_dir? and !has_project_droppr?
|
87
|
+
if args.count >= num_args
|
88
|
+
if greater_than
|
89
|
+
return true
|
90
|
+
elsif !greater_than and (args.count == num_args)
|
91
|
+
return true
|
92
|
+
else
|
93
|
+
return false
|
94
|
+
end
|
95
|
+
elsif wrong_args
|
96
|
+
error "You must include #{wrong_args}"
|
97
|
+
end
|
98
|
+
else
|
99
|
+
require_project_dir(action)
|
100
|
+
error "This app is already linked to appsendr." unless !has_project_droppr?
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
return false
|
105
|
+
end
|
106
|
+
|
107
|
+
def error(msg)
|
108
|
+
STDERR.puts(msg)
|
109
|
+
exit 1
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
unless String.method_defined?(:shellescape)
|
115
|
+
class String
|
116
|
+
def shellescape
|
117
|
+
empty? ? "''" : gsub(/([^A-Za-z0-9_\-.,:\/@\n])/n, '\\\\\\1').gsub(/\n/, "'\n'")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
data/lib/appsendr.rb
ADDED