nixenvironment 0.0.59 → 0.0.60
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 +4 -4
- data/Gemfile +1 -1
- data/README.md +14 -14
- data/bin/Config +2 -2
- data/bin/nixenvironment +420 -842
- data/legacy/CleanWorkingCopy.sh +69 -0
- data/legacy/Deploy.sh +44 -0
- data/legacy/DeployAPK.py +58 -0
- data/legacy/DeployIPA.sh +125 -0
- data/legacy/DetectSCM.sh +11 -0
- data/legacy/GenerateCodeCoverageForXCTests.sh +134 -0
- data/legacy/GenerateCodeDuplicationReport.sh +24 -0
- data/legacy/IncrementBuildNumber.py +129 -0
- data/legacy/LoadBuildEnvVars.sh +116 -0
- data/legacy/MakeTag.sh +94 -0
- data/legacy/RemoveTemporaryFiles.sh +9 -0
- data/legacy/SaveRevision.sh +122 -0
- data/legacy/UnityBuildAndroid.py +84 -0
- data/legacy/UnityBuildAutomationScripts/CommandLineReader.cs +130 -0
- data/legacy/UnityBuildAutomationScripts/NIXBuilder.cs +105 -0
- data/legacy/UnityBuildEnvVars.py +41 -0
- data/legacy/VerifyBinarySigning.py +80 -0
- data/legacy/svn-clean.pl +246 -0
- data/legacy/svncopy.pl +1134 -0
- data/lib/nixenvironment.rb +5 -0
- data/lib/nixenvironment/archiver.rb +690 -0
- data/lib/nixenvironment/build_env_vars_loader.rb +24 -0
- data/lib/nixenvironment/cmd_executor.rb +33 -0
- data/lib/nixenvironment/config.rb +127 -14
- data/lib/nixenvironment/git.rb +107 -0
- data/lib/nixenvironment/plist.rb +52 -0
- data/lib/nixenvironment/version.rb +1 -1
- data/lib/nixenvironment/xcodebuild.rb +167 -0
- data/nixenvironment.gemspec +6 -0
- data/utils/XcodeIconTagger/IconTagger +0 -0
- data/utils/XcodeIconTagger/masks/OneLineMask.png +0 -0
- data/utils/XcodeIconTagger/masks/TwoLineMask.png +0 -0
- data/utils/aapt +0 -0
- data/utils/gcovr +986 -0
- data/utils/identitieslist +0 -0
- data/utils/simian-2.3.33.jar +0 -0
- metadata +118 -2
data/lib/nixenvironment.rb
CHANGED
@@ -0,0 +1,690 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'fuzzy_match'
|
3
|
+
require 'tmpdir'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'set'
|
7
|
+
require 'openssl'
|
8
|
+
|
9
|
+
module Nixenvironment
|
10
|
+
class Archiver
|
11
|
+
ENTITLEMENTS_KEY = "Entitlements"
|
12
|
+
APPLICATION_IDENTIFIER_KEY = "application-identifier"
|
13
|
+
KEYCHAIN_ACCESS_GROUPS_KEY = "keychain-access-groups"
|
14
|
+
|
15
|
+
PROFILE_TYPE_DEVELOPER = 'developer'
|
16
|
+
PROFILE_TYPE_ADHOC = 'adhoc'
|
17
|
+
PROFILE_TYPE_APPSTORE = 'appstore'
|
18
|
+
AVAILABLE_PROFILE_TYPES = [PROFILE_TYPE_DEVELOPER, PROFILE_TYPE_ADHOC, PROFILE_TYPE_APPSTORE]
|
19
|
+
|
20
|
+
class ProfileInfo
|
21
|
+
attr_accessor :path, :name, :app_id, :device_count
|
22
|
+
|
23
|
+
def initialize(path, name, app_id, device_count)
|
24
|
+
@path = path
|
25
|
+
@name = name
|
26
|
+
@app_id = app_id
|
27
|
+
@device_count = device_count
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
"<'path=#{@path}', name='#{@name}', app_id='#{@app_id}', device_count=#{@device_count}>"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def make_signed_ipa
|
37
|
+
make_ipa(PROFILE_TYPE_DEVELOPER, nil, 'IPA_PRODUCT', 'IPA_BUNDLE_ID', 'NAME_FOR_DEPLOYMENT')
|
38
|
+
end
|
39
|
+
|
40
|
+
def make_resigned_ipa_for_device
|
41
|
+
make_ipa(PROFILE_TYPE_DEVELOPER, '-Resigned', 'IPA_PRODUCT_RESIGNED_DEVICE', 'IPA_BUNDLE_ID_RESIGNED_DEVICE', 'NAME_FOR_DEPLOYMENT_RESIGNED_DEVICE')
|
42
|
+
end
|
43
|
+
|
44
|
+
def make_resigned_ipa_for_adhoc
|
45
|
+
make_ipa(PROFILE_TYPE_ADHOC, '-Resigned-AdHoc', 'IPA_PRODUCT_RESIGNED_ADHOC', 'IPA_BUNDLE_ID_RESIGNED_ADHOC', 'NAME_FOR_DEPLOYMENT_RESIGNED_ADHOC')
|
46
|
+
end
|
47
|
+
|
48
|
+
def make_resigned_ipa_for_appstore
|
49
|
+
make_ipa(PROFILE_TYPE_APPSTORE, '-Resigned-Appstore', 'IPA_PRODUCT_RESIGNED_APPSTORE', 'IPA_BUNDLE_ID_RESIGNED_APPSTORE', 'NAME_FOR_DEPLOYMENT_RESIGNED_APPSTORE')
|
50
|
+
end
|
51
|
+
|
52
|
+
def make_macos_zip
|
53
|
+
build_env_vars = BuildEnvVarsLoader.load
|
54
|
+
built_products_dir = build_env_vars[BUILT_PRODUCTS_DIR_KEY].presence
|
55
|
+
executable_name = build_env_vars[EXECUTABLE_NAME_KEY].presence
|
56
|
+
target_name = build_env_vars[TARGET_NAME_KEY].presence
|
57
|
+
configuration = build_env_vars[CONFIGURATION_KEY].presence
|
58
|
+
app_product = build_env_vars[APP_PRODUCT_KEY].presence
|
59
|
+
|
60
|
+
new_ipa_name = "#{executable_name}-#{target_name}-#{configuration}"
|
61
|
+
new_ipa_path = File.join(built_products_dir, new_ipa_name) + ZIP_EXT
|
62
|
+
puts "IPA_PRODUCT = #{new_ipa_path}"
|
63
|
+
|
64
|
+
Dir.mktmpdir do |tmp_dir|
|
65
|
+
dest_app_dir = File.join(tmp_dir, new_ipa_name)
|
66
|
+
dest_app_product = File.join(dest_app_dir, File.basename(app_product))
|
67
|
+
|
68
|
+
puts "--> Create '#{dest_app_dir}' ..."
|
69
|
+
Dir.mkdir(dest_app_dir)
|
70
|
+
|
71
|
+
puts "--> Copy '#{app_product}' into '#{dest_app_product}' ..."
|
72
|
+
FileUtils.cp_r(app_product, dest_app_product)
|
73
|
+
|
74
|
+
if Dir.exist?(new_ipa_path)
|
75
|
+
puts "--> Remove old '#{new_ipa_path}' ..."
|
76
|
+
FileUtils.rm_rf(new_ipa_path)
|
77
|
+
end
|
78
|
+
|
79
|
+
puts "--> Zip '#{tmp_dir}' into '#{new_ipa_path}' ..."
|
80
|
+
Dir.chdir(tmp_dir) do
|
81
|
+
zip_success = system("/usr/bin/zip --symlinks --verbose --recurse-paths \"#{new_ipa_path}\" .")
|
82
|
+
raise unless zip_success
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
ipa_bundle_id = get_bundle_id(File.join(app_product, 'Contents'))
|
87
|
+
|
88
|
+
system("echo \"\n
|
89
|
+
IPA_PRODUCT='#{new_ipa_path}'
|
90
|
+
IPA_BUNDLE_ID='#{ipa_bundle_id}'
|
91
|
+
NAME_FOR_DEPLOYMENT='#{configuration}'
|
92
|
+
\" >> _last_build_vars.sh")
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def make_ipa(profile_type, ipa_product_suffix, ipa_product_key, ipa_bundle_id_key, name_for_deployment_key)
|
98
|
+
is_resigned = ipa_product_suffix.present? && ipa_product_suffix.include?('Resigned')
|
99
|
+
is_appstore = ipa_product_suffix.present? && ipa_product_suffix.include?('Appstore')
|
100
|
+
|
101
|
+
build_env_vars = BuildEnvVarsLoader.load
|
102
|
+
built_products_dir = build_env_vars[BUILT_PRODUCTS_DIR_KEY].presence
|
103
|
+
executable_name = build_env_vars[EXECUTABLE_NAME_KEY].presence
|
104
|
+
target_name = build_env_vars[TARGET_NAME_KEY].presence
|
105
|
+
configuration = build_env_vars[CONFIGURATION_KEY].presence
|
106
|
+
app_product = build_env_vars[APP_PRODUCT_KEY].presence
|
107
|
+
|
108
|
+
watchkit_app_relative_product = build_env_vars['WATCHKIT_APP_RELATIVE_PRODUCT'].presence
|
109
|
+
watchkit_extension_relative_product = build_env_vars['WATCHKIT_EXTENSION_RELATIVE_PRODUCT'].presence
|
110
|
+
widget_relative_product = build_env_vars['WIDGET_RELATIVE_PRODUCT'].presence
|
111
|
+
|
112
|
+
resigned_bundle_id = build_env_vars[RESIGNED_BUNDLE_ID_KEY].presence
|
113
|
+
resigned_watchkit_app_bundle_id = build_env_vars[RESIGNED_WATCHKIT_APP_BUNDLE_ID_KEY].presence
|
114
|
+
resigned_watchkit_extension_bundle_id = build_env_vars[RESIGNED_WATCHKIT_EXTENSION_BUNDLE_ID_KEY].presence
|
115
|
+
resigned_widget_bundle_id = build_env_vars[RESIGNED_WIDGET_BUNDLE_ID_KEY].presence
|
116
|
+
|
117
|
+
resigned_bundle_name = build_env_vars[RESIGNED_BUNDLE_NAME_KEY].presence
|
118
|
+
resigned_entitlements_path = build_env_vars[RESIGNED_ENTITLEMENTS_PATH_KEY].presence
|
119
|
+
resigned_watchkit_extension_entitlements_path = build_env_vars[RESIGNED_WATCHKIT_EXTENSION_ENTITLEMENTS_PATH_KEY].presence
|
120
|
+
resigned_widget_entitlements_path = build_env_vars[RESIGNED_WIDGET_ENTITLEMENTS_PATH_KEY].presence
|
121
|
+
|
122
|
+
ipa_product = "#{built_products_dir}/#{executable_name}-#{target_name}-#{configuration}#{ipa_product_suffix}.ipa"
|
123
|
+
puts "#{ipa_product_key} = #{ipa_product}"
|
124
|
+
|
125
|
+
if is_appstore
|
126
|
+
app_name = File.basename(app_product)
|
127
|
+
temp_app_product = File.join(Dir.tmpdir, app_name)
|
128
|
+
|
129
|
+
FileUtils.cp_r(app_product, temp_app_product)
|
130
|
+
|
131
|
+
set_plist_values_in_app_path(temp_app_product, 'Configuration' => 'Appstore')
|
132
|
+
end
|
133
|
+
|
134
|
+
@valid_keychain_identities_cache = get_valid_keychain_identities
|
135
|
+
|
136
|
+
resign(app_product, ipa_product, watchkit_app_relative_product, watchkit_extension_relative_product, widget_relative_product,
|
137
|
+
resigned_bundle_id, resigned_watchkit_app_bundle_id, resigned_watchkit_extension_bundle_id, resigned_widget_bundle_id,
|
138
|
+
resigned_bundle_name, resigned_entitlements_path, resigned_watchkit_extension_entitlements_path, resigned_widget_entitlements_path, profile_type)
|
139
|
+
|
140
|
+
ipa_bundle_id = is_resigned ? resigned_bundle_id : get_bundle_id(app_product)
|
141
|
+
|
142
|
+
system("echo \"\n# generated by Nixenvironment:
|
143
|
+
#{ipa_product_key}='#{ipa_product}'
|
144
|
+
#{ipa_bundle_id_key}='#{ipa_bundle_id}'
|
145
|
+
#{name_for_deployment_key}='#{configuration}'
|
146
|
+
\" >> _last_build_vars.sh")
|
147
|
+
|
148
|
+
FileUtils.rm_rf(temp_app_product) if is_appstore
|
149
|
+
end
|
150
|
+
|
151
|
+
# Resigns specified .app product and packages it into .ipa file. Finds best matching provisioning profile based on specified options.
|
152
|
+
# app_product_path => full path to input .app folder
|
153
|
+
# new_ipa_path => full path to output .ipa file
|
154
|
+
# profile_type => type of profile to resign with AVAILABLE_PROFILE_TYPES
|
155
|
+
def resign(app_product_path, new_ipa_path, watchkit_app_relative_product_path = nil, watchkit_extension_relative_product_path = nil,
|
156
|
+
widget_relative_product_path = nil, new_bundle_id = nil, new_watchkit_app_bundle_id = nil, new_watchkit_extension_bundle_id = nil,
|
157
|
+
new_widget_bundle_id = nil, new_bundle_name = nil, new_entitlements_path = nil, new_watchkit_extension_entitlements_path = nil,
|
158
|
+
new_widget_entitlements_path = nil, profile_type = PROFILE_TYPE_DEVELOPER)
|
159
|
+
raise "Unknown profile type '#{profile_type}'! Must be from #{AVAILABLE_PROFILE_TYPES}" unless AVAILABLE_PROFILE_TYPES.include?(profile_type)
|
160
|
+
|
161
|
+
new_bundle_id ||= get_bundle_id(app_product_path)
|
162
|
+
new_bundle_name ||= get_bundle_name(app_product_path)
|
163
|
+
|
164
|
+
new_watchkit_app_bundle_id ||= get_bundle_id(File.join(app_product_path, watchkit_app_relative_product_path)) if watchkit_app_relative_product_path.present?
|
165
|
+
new_watchkit_extension_bundle_id ||= get_bundle_id(File.join(app_product_path, watchkit_extension_relative_product_path)) if watchkit_extension_relative_product_path.present?
|
166
|
+
new_widget_bundle_id ||= get_bundle_id(File.join(app_product_path, widget_relative_product_path)) if widget_relative_product_path.present?
|
167
|
+
|
168
|
+
profile_info, identity_name = find_profile_info_and_identity_name(profile_type, new_bundle_id)
|
169
|
+
|
170
|
+
puts "==> Sign with profile '".bold + profile_info.name.blink.underline + "', identity '" + identity_name.underline + "' ...".bold
|
171
|
+
|
172
|
+
watchkit_app_profile_info = nil
|
173
|
+
watchkit_app_identity_name = nil
|
174
|
+
watchkit_extension_profile_info = nil
|
175
|
+
watchkit_extension_identity_name = nil
|
176
|
+
widget_profile_info = nil
|
177
|
+
widget_identity_name = nil
|
178
|
+
|
179
|
+
if watchkit_app_relative_product_path.present?
|
180
|
+
watchkit_app_profile_info, watchkit_app_identity_name = find_profile_info_and_identity_name(profile_type, new_watchkit_app_bundle_id)
|
181
|
+
puts "==> Sign watchkit_app with profile '".bold + watchkit_app_profile_info.name.blink.underline + "', identity '".bold + watchkit_app_identity_name.underline + "' ...".bold
|
182
|
+
end
|
183
|
+
|
184
|
+
if watchkit_extension_relative_product_path.present?
|
185
|
+
watchkit_extension_profile_info, watchkit_extension_identity_name = find_profile_info_and_identity_name(profile_type, new_watchkit_extension_bundle_id)
|
186
|
+
puts "==> Sign watchkit_extension with profile '".bold + watchkit_extension_profile_info.name.blink.underline + "', identity '".bold + watchkit_extension_identity_name.underline + "' ...".bold
|
187
|
+
end
|
188
|
+
|
189
|
+
if widget_relative_product_path.present?
|
190
|
+
widget_profile_info, widget_identity_name = find_profile_info_and_identity_name(profile_type, new_widget_bundle_id)
|
191
|
+
puts "==> Sign widget with profile '".bold + widget_profile_info.name.blink.underline + "', identity '".bold + widget_identity_name.underline + "' ...".bold
|
192
|
+
end
|
193
|
+
|
194
|
+
watchkit_app_profile_path = watchkit_app_profile_info ? watchkit_app_profile_info.path : nil
|
195
|
+
watchkit_extension_profile_path = watchkit_extension_profile_info ? watchkit_extension_profile_info.path : nil
|
196
|
+
widget_profile_path = widget_profile_info ? widget_profile_info.path : nil
|
197
|
+
|
198
|
+
package_application(app_product_path, watchkit_app_relative_product_path, watchkit_extension_relative_product_path, widget_relative_product_path, new_ipa_path,
|
199
|
+
new_bundle_id, new_watchkit_app_bundle_id, new_watchkit_extension_bundle_id, new_widget_bundle_id, new_bundle_name,
|
200
|
+
new_entitlements_path, new_watchkit_extension_entitlements_path, new_widget_entitlements_path,
|
201
|
+
profile_info.path, identity_name, watchkit_app_profile_path, watchkit_app_identity_name, watchkit_extension_profile_path, watchkit_extension_identity_name,
|
202
|
+
widget_profile_path, widget_identity_name)
|
203
|
+
end
|
204
|
+
|
205
|
+
def find_profile_info_and_identity_name(profile_type, new_bundle_id)
|
206
|
+
profiles_and_identities = case profile_type
|
207
|
+
when PROFILE_TYPE_DEVELOPER then get_matching_developer_profiles_and_identities(new_bundle_id)
|
208
|
+
when PROFILE_TYPE_ADHOC then get_matching_adhoc_profiles_and_identities(new_bundle_id)
|
209
|
+
when PROFILE_TYPE_APPSTORE then get_matching_appstore_profiles_and_identities(new_bundle_id)
|
210
|
+
else raise "Unknown profile_type '#{profile_type}'!"
|
211
|
+
end
|
212
|
+
|
213
|
+
raise 'No mathching profiles found, read logs for more info!' if profiles_and_identities.blank?
|
214
|
+
|
215
|
+
puts_header '--> Matching profiles and identities:'
|
216
|
+
profiles_and_identities.each { |item| pp item.first.to_s, item.last }
|
217
|
+
|
218
|
+
puts "--> Looking for the best match among found profiles, based on similarity between profile's app id and desired bundle id ..."
|
219
|
+
best_match = find_best_match_for_bundle_id(profiles_and_identities, new_bundle_id)
|
220
|
+
puts_header "--> Best match: #{best_match}"
|
221
|
+
|
222
|
+
profile_info = best_match.first
|
223
|
+
identity_name = best_match.last
|
224
|
+
|
225
|
+
return profile_info, identity_name
|
226
|
+
end
|
227
|
+
|
228
|
+
def find_best_match_for_bundle_id(profiles_and_identities, new_bundle_id)
|
229
|
+
profiles_and_identities.max_by { |item| app_id_similarity_to_new_bundle_id(item, new_bundle_id) }
|
230
|
+
end
|
231
|
+
|
232
|
+
def app_id_similarity_to_new_bundle_id(match, new_bundle_id)
|
233
|
+
app_id = match.first.app_id
|
234
|
+
winner, dices_coefficient_similar, _levenshtein_similar = FuzzyMatch.new([new_bundle_id]).find_with_score(app_id)
|
235
|
+
dices_coefficient_similar = 0 if winner.blank?
|
236
|
+
puts "\tsimilarity ratio: '#{new_bundle_id}' <--> '#{app_id}' = #{dices_coefficient_similar}"
|
237
|
+
dices_coefficient_similar
|
238
|
+
end
|
239
|
+
|
240
|
+
# Re-signs and packages specified app product.
|
241
|
+
# Does NOT check that new bundle id corresponds to profile.
|
242
|
+
# Automatically constructs new entitlements in case they are not specified.
|
243
|
+
# new_entitlements_path, new_watchkit_extension_entitlements_path and new_widget_entitlements_path can be set to None or ''.
|
244
|
+
# In this case the entitlements will be taken from the exisitng product or generated automatically.
|
245
|
+
# If re-signing with a distribution profile and get-task-allow is set to true, the AppStore will reject the submission.
|
246
|
+
# So the function automatically fixes the value of this entitlements field.
|
247
|
+
def package_application(app_product_path, watchkit_app_relative_product_path, watchkit_extension_relative_product_path, widget_relative_product_path, new_ipa_path,
|
248
|
+
new_bundle_id, new_watchkit_app_bundle_id, new_watchkit_extension_bundle_id, new_widget_bundle_id, new_bundle_name,
|
249
|
+
new_entitlements_path, new_watchkit_extension_entitlements_path, new_widget_entitlements_path,
|
250
|
+
profile_path, identity_name, watchkit_app_profile_path, watchkit_app_identity_name, watchkit_extension_profile_path, watchkit_extension_identity_name,
|
251
|
+
widget_profile_path, widget_identity_name)
|
252
|
+
Dir.mktmpdir do |tmp_dir|
|
253
|
+
dest_app_dir = File.join(tmp_dir, "Payload")
|
254
|
+
dest_app_product_path = File.join(dest_app_dir, File.basename(app_product_path))
|
255
|
+
|
256
|
+
puts "--> Create '#{dest_app_dir}' ..."
|
257
|
+
Dir.mkdir(dest_app_dir)
|
258
|
+
|
259
|
+
puts "--> Copy '#{app_product_path}' into '#{dest_app_product_path}' ..."
|
260
|
+
FileUtils.cp_r(app_product_path, dest_app_product_path)
|
261
|
+
|
262
|
+
# replace provision, rename bundle_id and bundle_name
|
263
|
+
is_provision_replaced = replace_provision(dest_app_product_path, profile_path)
|
264
|
+
is_bundle_id_or_name_changed = rename_bundle_id_and_name(dest_app_product_path, new_bundle_id, new_bundle_name)
|
265
|
+
|
266
|
+
# replace provision, rename bundle_id for watchkit app
|
267
|
+
if watchkit_app_relative_product_path.present?
|
268
|
+
dest_watchkit_app_product_path = File.join(dest_app_product_path, watchkit_app_relative_product_path)
|
269
|
+
_is_watchkit_app_provision_replaced = replace_provision(dest_watchkit_app_product_path, watchkit_app_profile_path)
|
270
|
+
_is_watchkit_app_bundle_id = rename_bundle_id(dest_watchkit_app_product_path, new_watchkit_app_bundle_id)
|
271
|
+
end
|
272
|
+
|
273
|
+
is_watchkit_extension_provision_replaced = nil
|
274
|
+
is_watchkit_extension_bundle_id_or_name_changed = nil
|
275
|
+
|
276
|
+
dest_watchkit_extension_product_path = nil
|
277
|
+
|
278
|
+
# replace provision, rename bundle_id and bundle_name for watchkit extension
|
279
|
+
if watchkit_extension_relative_product_path.present?
|
280
|
+
dest_watchkit_extension_product_path = File.join(dest_app_product_path, watchkit_extension_relative_product_path)
|
281
|
+
is_watchkit_extension_provision_replaced = replace_provision(dest_watchkit_extension_product_path, watchkit_extension_profile_path)
|
282
|
+
is_watchkit_extension_bundle_id_or_name_changed = rename_bundle_id_and_name(dest_watchkit_extension_product_path, new_watchkit_extension_bundle_id, nil)
|
283
|
+
end
|
284
|
+
|
285
|
+
is_widget_provision_replaced = nil
|
286
|
+
is_widget_bundle_id_or_name_changed = nil
|
287
|
+
|
288
|
+
dest_widget_product_path = nil
|
289
|
+
|
290
|
+
# replace provision, rename bundle_id and bundle_name for widget
|
291
|
+
if widget_relative_product_path.present?
|
292
|
+
dest_widget_product_path = File.join(dest_app_product_path, widget_relative_product_path)
|
293
|
+
is_widget_provision_replaced = replace_provision(dest_widget_product_path, widget_profile_path)
|
294
|
+
is_widget_bundle_id_or_name_changed = rename_bundle_id_and_name(dest_widget_product_path, new_widget_bundle_id, nil)
|
295
|
+
end
|
296
|
+
|
297
|
+
# codesign watchkit extension
|
298
|
+
if watchkit_extension_relative_product_path.present?
|
299
|
+
codesign(watchkit_extension_identity_name, new_watchkit_extension_entitlements_path, is_watchkit_extension_provision_replaced,
|
300
|
+
is_watchkit_extension_bundle_id_or_name_changed, dest_watchkit_extension_product_path, watchkit_extension_profile_path, new_watchkit_extension_bundle_id)
|
301
|
+
end
|
302
|
+
|
303
|
+
# codesign widget
|
304
|
+
if widget_relative_product_path.present?
|
305
|
+
codesign(widget_identity_name, new_widget_entitlements_path, is_widget_provision_replaced, is_widget_bundle_id_or_name_changed,
|
306
|
+
dest_widget_product_path, widget_profile_path, new_widget_bundle_id)
|
307
|
+
end
|
308
|
+
|
309
|
+
codesign(identity_name, new_entitlements_path, is_provision_replaced, is_bundle_id_or_name_changed, dest_app_product_path, profile_path, new_bundle_id)
|
310
|
+
|
311
|
+
if Dir.exist?(new_ipa_path)
|
312
|
+
puts "--> Remove old '#{new_ipa_path}' ..."
|
313
|
+
FileUtils.rm(new_ipa_path)
|
314
|
+
end
|
315
|
+
|
316
|
+
puts "--> Zip '#{tmp_dir}' into '#{new_ipa_path}' ..."
|
317
|
+
Dir.chdir(tmp_dir) { system("/usr/bin/zip --symlinks --verbose --recurse-paths '#{new_ipa_path}' .") }
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def codesign(identity_name, new_entitlements_path, is_provision_replaced, is_bundle_id_or_name_changed, dest_app_product_path, profile_path, new_bundle_id)
|
322
|
+
codesign_args = ['/usr/bin/codesign', '--force', '--sign', "'#{identity_name}'"]
|
323
|
+
|
324
|
+
# now let's figure out the entitlements...
|
325
|
+
if new_entitlements_path.present?
|
326
|
+
# a) use explicitly set entitlements
|
327
|
+
puts_header "--> Using explicitly set entitlements from '#{new_entitlements_path}'"
|
328
|
+
|
329
|
+
# make a copy to not mess up the original file
|
330
|
+
selected_entitlements_path = Tempfile.new('selected_entitlements').path
|
331
|
+
FileUtils.cp(new_entitlements_path, selected_entitlements_path)
|
332
|
+
else
|
333
|
+
should_generate_entitlements_manually = is_provision_replaced || is_bundle_id_or_name_changed
|
334
|
+
|
335
|
+
unless should_generate_entitlements_manually
|
336
|
+
# b) existing entitlements are OK
|
337
|
+
entitlements_file = get_entitlements_from_app(dest_app_product_path)
|
338
|
+
selected_entitlements_path = entitlements_file.path
|
339
|
+
|
340
|
+
# leave only plist data in file
|
341
|
+
plist = get_plist_from_file(selected_entitlements_path)
|
342
|
+
plist.save
|
343
|
+
|
344
|
+
puts_header '--> Using existing entitlements'
|
345
|
+
else
|
346
|
+
# c) no entitlements is bad, so we will construct them manually
|
347
|
+
selected_entitlements_path = generate_temp_entitlements_file_from_profile(profile_path, new_bundle_id)
|
348
|
+
|
349
|
+
puts_header "--> Using automatically generated entitlements from '#{selected_entitlements_path}'"
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# crucial for submission
|
354
|
+
fix_get_task_allow(selected_entitlements_path, identity_name)
|
355
|
+
|
356
|
+
puts_warning '--> Entitlements:'
|
357
|
+
puts File.read(selected_entitlements_path)
|
358
|
+
|
359
|
+
codesign_args.concat(['--entitlements', "'#{selected_entitlements_path}'", "'#{dest_app_product_path}'"])
|
360
|
+
|
361
|
+
begin
|
362
|
+
puts_header "--> Codesign with params #{codesign_args} ..."
|
363
|
+
raise unless system(codesign_args.join(' '))
|
364
|
+
ensure
|
365
|
+
puts "--> Remove temp entitlements file '#{selected_entitlements_path}'"
|
366
|
+
FileUtils.rm(selected_entitlements_path)
|
367
|
+
end
|
368
|
+
|
369
|
+
check_signature(dest_app_product_path)
|
370
|
+
end
|
371
|
+
|
372
|
+
def check_signature(app_product_path)
|
373
|
+
puts '--> Check signature ...'
|
374
|
+
raise unless system("/usr/bin/codesign --verify --no-strict -vvvv '#{app_product_path}'")
|
375
|
+
end
|
376
|
+
|
377
|
+
def replace_provision(app_product_path, provision_path)
|
378
|
+
embedded_provision_path = File.join(app_product_path, 'embedded.mobileprovision')
|
379
|
+
is_provision_the_same = FileUtils.cmp(provision_path, embedded_provision_path)
|
380
|
+
|
381
|
+
unless is_provision_the_same
|
382
|
+
puts "--> Embed profile '#{provision_path}' ..."
|
383
|
+
FileUtils.cp(provision_path, embedded_provision_path)
|
384
|
+
end
|
385
|
+
|
386
|
+
!is_provision_the_same
|
387
|
+
end
|
388
|
+
|
389
|
+
def rename_bundle_id_and_name(app_product_path, new_bundle_id, new_bundle_name)
|
390
|
+
rename_bundle_id(app_product_path, new_bundle_id) && rename_bundle_name(app_product_path, new_bundle_name)
|
391
|
+
end
|
392
|
+
|
393
|
+
def rename_bundle_id(app_product_path, new_bundle_id)
|
394
|
+
original_bundle_id = get_bundle_id(app_product_path)
|
395
|
+
is_bundle_id_the_same = false
|
396
|
+
|
397
|
+
if new_bundle_id == original_bundle_id
|
398
|
+
puts_header "--> Bundle id '#{new_bundle_id}' will not be modified"
|
399
|
+
is_bundle_id_the_same = true
|
400
|
+
else
|
401
|
+
puts_header "--> Rename bundle id from '#{original_bundle_id}' into '#{new_bundle_id} in '#{app_product_path}' Info.plist ..."
|
402
|
+
set_plist_values_in_app_path(app_product_path, 'CFBundleIdentifier' => new_bundle_id)
|
403
|
+
end
|
404
|
+
|
405
|
+
is_bundle_id_the_same
|
406
|
+
end
|
407
|
+
|
408
|
+
def rename_bundle_name(app_product_path, new_bundle_name)
|
409
|
+
original_bundle_name = get_bundle_name(app_product_path)
|
410
|
+
original_bundle_display_name = get_bundle_display_name(app_product_path)
|
411
|
+
|
412
|
+
is_bundle_name_the_same = false
|
413
|
+
|
414
|
+
if new_bundle_name == original_bundle_name && new_bundle_name == original_bundle_display_name
|
415
|
+
puts_header "--> Bundle name '#{new_bundle_name}' will not be modified"
|
416
|
+
is_bundle_name_the_same = true
|
417
|
+
else
|
418
|
+
puts_header "--> Rename bundle name/bundle display name from '#{original_bundle_name}'/'#{original_bundle_display_name}' into '#{new_bundle_name}' in '#{app_product_path}' Info.plist ..."
|
419
|
+
set_plist_values_in_app_path(app_product_path, { 'CFBundleName' => new_bundle_name, 'CFBundleDisplayName' => new_bundle_name } )
|
420
|
+
end
|
421
|
+
|
422
|
+
is_bundle_name_the_same
|
423
|
+
end
|
424
|
+
|
425
|
+
def get_bundle_id(app_product_path)
|
426
|
+
plist_value_from_app_path(app_product_path, 'CFBundleIdentifier')
|
427
|
+
end
|
428
|
+
|
429
|
+
def get_bundle_name(app_product_path)
|
430
|
+
plist_value_from_app_path(app_product_path, 'CFBundleName')
|
431
|
+
end
|
432
|
+
|
433
|
+
def get_bundle_display_name(app_product_path)
|
434
|
+
plist_value_from_app_path(app_product_path, 'CFBundleDisplayName')
|
435
|
+
end
|
436
|
+
|
437
|
+
def plist_value_from_app_path(app_product_path, key)
|
438
|
+
info_plist_path = File.join(app_product_path, 'Info.plist')
|
439
|
+
info_plist = Plist.from_file(info_plist_path)
|
440
|
+
info_plist[key]
|
441
|
+
end
|
442
|
+
|
443
|
+
def set_plist_values_in_app_path(app_product_path, hash)
|
444
|
+
info_plist_path = File.join(app_product_path, 'Info.plist')
|
445
|
+
info_plist = Plist.from_file(info_plist_path)
|
446
|
+
hash.each { |key, value| info_plist[key] = value }
|
447
|
+
info_plist.save
|
448
|
+
end
|
449
|
+
|
450
|
+
# Returns None if entitlements cannot be read (e.g. when signature is modified), otherwise - temp file with entitlements.
|
451
|
+
# User is responsible for removing temp file.
|
452
|
+
def get_entitlements_from_app(app_product_path)
|
453
|
+
entitlements_file = Tempfile.new('entitlements_file')
|
454
|
+
|
455
|
+
puts "--> Copy entitlements from '#{app_product_path}' into '#{entitlements_file.path}' ..."
|
456
|
+
|
457
|
+
codesign_success = system("codesign -d '#{app_product_path}' --entitlements '#{entitlements_file.path}'")
|
458
|
+
|
459
|
+
unless codesign_success
|
460
|
+
entitlements_file.close
|
461
|
+
entitlements_file.unlink
|
462
|
+
end
|
463
|
+
|
464
|
+
entitlements_file
|
465
|
+
end
|
466
|
+
|
467
|
+
def app_id_prefix_from_profile(profile_path)
|
468
|
+
_profile_name, app_id, _certs, _device_count = parse_profile(profile_path)
|
469
|
+
app_id.partition('.').first
|
470
|
+
end
|
471
|
+
|
472
|
+
def identity_is_for_development(identity_name)
|
473
|
+
identity_name.start_with?('iPhone Developer')
|
474
|
+
end
|
475
|
+
|
476
|
+
def identity_is_for_distribution(identity_name)
|
477
|
+
identity_name.start_with?('iPhone Distribution')
|
478
|
+
end
|
479
|
+
|
480
|
+
# User is responsible for removing temp file. Always sets the same get-task-allow.
|
481
|
+
def generate_temp_entitlements_file_from_profile(profile_path, bundle_id)
|
482
|
+
generated_entitlements_path = Tempfile.new('generated_entitlements').path
|
483
|
+
|
484
|
+
puts_header "--> Automatically generate new entitlements at '#{generated_entitlements_path}' ..."
|
485
|
+
|
486
|
+
app_id_prefix = app_id_prefix_from_profile(profile_path)
|
487
|
+
application_identifier = app_id_prefix + "." + bundle_id
|
488
|
+
|
489
|
+
plist = get_plist_from_file(profile_path)
|
490
|
+
profile_entitlements = plist[ENTITLEMENTS_KEY]
|
491
|
+
profile_entitlements[APPLICATION_IDENTIFIER_KEY] = application_identifier
|
492
|
+
profile_entitlements[KEYCHAIN_ACCESS_GROUPS_KEY][0] = application_identifier
|
493
|
+
|
494
|
+
entitlements = Plist.from_hash(profile_entitlements)
|
495
|
+
entitlements.save(generated_entitlements_path)
|
496
|
+
|
497
|
+
puts '--> Lint new entitlements ...'
|
498
|
+
raise unless system("/usr/bin/plutil -lint '#{generated_entitlements_path}'")
|
499
|
+
|
500
|
+
generated_entitlements_path
|
501
|
+
end
|
502
|
+
|
503
|
+
def fix_get_task_allow(entitlements_path, identity_name)
|
504
|
+
puts '--> Fix get-task-allow if needed ...'
|
505
|
+
|
506
|
+
plist = Plist.from_file(entitlements_path)
|
507
|
+
|
508
|
+
# this entitlements field must be set to False for distribution
|
509
|
+
new_value = !identity_is_for_distribution(identity_name)
|
510
|
+
plist['get-task-allow'] = new_value
|
511
|
+
plist.save
|
512
|
+
|
513
|
+
puts "--> get-task-allow = #{new_value}"
|
514
|
+
end
|
515
|
+
|
516
|
+
# Use it to match only developer profiles.
|
517
|
+
def get_matching_developer_profiles_and_identities(new_bundle_id)
|
518
|
+
matches = []
|
519
|
+
|
520
|
+
get_matching_profiles_and_identities(new_bundle_id) do |match|
|
521
|
+
identity_name = match[1]
|
522
|
+
|
523
|
+
if identity_is_for_development(identity_name)
|
524
|
+
matches << match
|
525
|
+
else
|
526
|
+
puts_warning "\t--> Skipping, because looking only for developer profiles"
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
matches
|
531
|
+
end
|
532
|
+
|
533
|
+
# Use it to match only Appstore distribution profiles.
|
534
|
+
def get_matching_appstore_profiles_and_identities(new_bundle_id)
|
535
|
+
matches = []
|
536
|
+
|
537
|
+
get_matching_profiles_and_identities(new_bundle_id) do |match|
|
538
|
+
device_count = match.first.device_count
|
539
|
+
identity_name = match.last
|
540
|
+
|
541
|
+
if !identity_is_for_distribution(identity_name)
|
542
|
+
puts_warning "\t--> Skipping, because looking only for Appstore distribution profiles"
|
543
|
+
elsif device_count > 0
|
544
|
+
puts_warning "\t--> Skipping, because profile contains devices"
|
545
|
+
else
|
546
|
+
matches << match
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
matches
|
551
|
+
end
|
552
|
+
|
553
|
+
# Use it to match only AdHoc distribution profiles.
|
554
|
+
def get_matching_adhoc_profiles_and_identities(new_bundle_id)
|
555
|
+
matches = []
|
556
|
+
|
557
|
+
get_matching_profiles_and_identities(new_bundle_id) do |match|
|
558
|
+
device_count = match.first.device_count
|
559
|
+
identity_name = match.last
|
560
|
+
|
561
|
+
if !identity_is_for_distribution(identity_name)
|
562
|
+
puts_warning "\t--> Skipping, because looking only for AdHoc distribution profiles"
|
563
|
+
elsif device_count == 0
|
564
|
+
puts_warning "\t--> Skipping, because profile does not contain devices"
|
565
|
+
else
|
566
|
+
matches << match
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
matches
|
571
|
+
end
|
572
|
+
|
573
|
+
# Generator. Finds matching profile and identity based on specified bundle id. Matches only valid identities.
|
574
|
+
def get_matching_profiles_and_identities(new_bundle_id)
|
575
|
+
profiles_path = File.expand_path('~/Library/MobileDevice/Provisioning Profiles/')
|
576
|
+
|
577
|
+
puts_bold "--> Scan profiles at '#{profiles_path}' ..."
|
578
|
+
|
579
|
+
Dir.foreach(profiles_path) do |filename|
|
580
|
+
next if file_is_not_profile(filename)
|
581
|
+
|
582
|
+
print_underline "Profile '#{filename}', "
|
583
|
+
|
584
|
+
profile_path = File.join(profiles_path, filename)
|
585
|
+
profile_name, app_id, certs, device_count = parse_profile(profile_path)
|
586
|
+
|
587
|
+
puts "name = '#{profile_name}', app id='#{app_id}', #{device_count} device(s)"
|
588
|
+
|
589
|
+
unless bundle_id_corresponds_to_app_id(new_bundle_id, app_id)
|
590
|
+
puts_warning "\t--> Can't use this profile, because app id '#{app_id}' doesn't allow new bundle id '#{new_bundle_id}'"
|
591
|
+
next
|
592
|
+
end
|
593
|
+
|
594
|
+
puts_header "\t--> Profile's app id '#{app_id}' allows new bundle id '#{new_bundle_id}'"
|
595
|
+
puts_bold "\t--> Scan certs ..."
|
596
|
+
|
597
|
+
get_matching_identities_from_certs(certs) do |identity_name|
|
598
|
+
profile_info = ProfileInfo.new(profile_path, profile_name, app_id, device_count)
|
599
|
+
match = [profile_info, identity_name]
|
600
|
+
|
601
|
+
puts_header "\t--> Match found: #{profile_info.to_s}, identity_name='#{identity_name}'"
|
602
|
+
|
603
|
+
yield match
|
604
|
+
end
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
def file_is_not_profile(filename)
|
609
|
+
File.extname(filename) != '.mobileprovision'
|
610
|
+
end
|
611
|
+
|
612
|
+
def parse_profile(profile_path)
|
613
|
+
plist = get_plist_from_file(profile_path)
|
614
|
+
|
615
|
+
profile_name = plist['Name']
|
616
|
+
app_id = plist[ENTITLEMENTS_KEY][APPLICATION_IDENTIFIER_KEY]
|
617
|
+
certs = plist['DeveloperCertificates']
|
618
|
+
devices = plist['ProvisionedDevices']
|
619
|
+
device_count = devices.present? ? devices.size : 0
|
620
|
+
|
621
|
+
return profile_name, app_id, certs, device_count
|
622
|
+
end
|
623
|
+
|
624
|
+
def get_plist_from_file(path)
|
625
|
+
content = File.read(path)
|
626
|
+
|
627
|
+
plist_start_index = content.index('<?xml')
|
628
|
+
plist_end_index = content.index('</plist>') + '</plist>'.length
|
629
|
+
profile_plist_str = content[plist_start_index...plist_end_index]
|
630
|
+
|
631
|
+
Plist.from_str(profile_plist_str)
|
632
|
+
end
|
633
|
+
|
634
|
+
def bundle_id_corresponds_to_app_id(bundle_id, app_id)
|
635
|
+
app_id_without_seed_id = app_id[app_id.index('.') + 1..-1]
|
636
|
+
app_id_without_seed_id_regexp = app_id_without_seed_id.gsub('.', '\.').gsub('*', '.*')
|
637
|
+
|
638
|
+
bundle_id =~ /#{app_id_without_seed_id_regexp}/
|
639
|
+
end
|
640
|
+
|
641
|
+
# Generator. Returns list of valid identities based on the list of embedded certs from the profile file.
|
642
|
+
def get_matching_identities_from_certs(certs)
|
643
|
+
certs.each do |cert|
|
644
|
+
key = OpenSSL::X509::Certificate.new(cert)
|
645
|
+
# key.subject.to_a => [["UID", ..., ...], ["CN", identity_name, ...], ...]
|
646
|
+
identity_name = key.subject.to_a[1][1]
|
647
|
+
|
648
|
+
print "\tIdentity '#{identity_name}'..."
|
649
|
+
|
650
|
+
unless identity_is_valid(identity_name)
|
651
|
+
puts_warning 'invalid'
|
652
|
+
next
|
653
|
+
end
|
654
|
+
|
655
|
+
puts_header 'valid!'
|
656
|
+
|
657
|
+
yield identity_name
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
def identity_is_valid(identity_name)
|
662
|
+
@valid_keychain_identities_cache.include?(identity_name)
|
663
|
+
end
|
664
|
+
|
665
|
+
def get_valid_keychain_identities()
|
666
|
+
keychain_name = 'XCodeKeys'
|
667
|
+
keychain_path = %x[ security list | grep '#{keychain_name}' | sed 's/\"//g' | xargs ].strip
|
668
|
+
security_report = %x[ #{IDENTITIESLIST_UTILITY_PATH} -k #{keychain_path} ]
|
669
|
+
entries = security_report.split(/\n/)
|
670
|
+
entries.to_set
|
671
|
+
end
|
672
|
+
|
673
|
+
def puts_bold(msg)
|
674
|
+
puts msg.bold
|
675
|
+
end
|
676
|
+
|
677
|
+
def print_underline(msg)
|
678
|
+
print msg.underline
|
679
|
+
end
|
680
|
+
|
681
|
+
def puts_header(msg)
|
682
|
+
puts_bold msg.blue
|
683
|
+
end
|
684
|
+
|
685
|
+
def puts_warning(msg)
|
686
|
+
puts_bold msg.yellow.on_black
|
687
|
+
end
|
688
|
+
end
|
689
|
+
end
|
690
|
+
end
|