appsendr 0.0.1
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.
- 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