nixenvironment 0.0.59 → 0.0.60

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/README.md +14 -14
  4. data/bin/Config +2 -2
  5. data/bin/nixenvironment +420 -842
  6. data/legacy/CleanWorkingCopy.sh +69 -0
  7. data/legacy/Deploy.sh +44 -0
  8. data/legacy/DeployAPK.py +58 -0
  9. data/legacy/DeployIPA.sh +125 -0
  10. data/legacy/DetectSCM.sh +11 -0
  11. data/legacy/GenerateCodeCoverageForXCTests.sh +134 -0
  12. data/legacy/GenerateCodeDuplicationReport.sh +24 -0
  13. data/legacy/IncrementBuildNumber.py +129 -0
  14. data/legacy/LoadBuildEnvVars.sh +116 -0
  15. data/legacy/MakeTag.sh +94 -0
  16. data/legacy/RemoveTemporaryFiles.sh +9 -0
  17. data/legacy/SaveRevision.sh +122 -0
  18. data/legacy/UnityBuildAndroid.py +84 -0
  19. data/legacy/UnityBuildAutomationScripts/CommandLineReader.cs +130 -0
  20. data/legacy/UnityBuildAutomationScripts/NIXBuilder.cs +105 -0
  21. data/legacy/UnityBuildEnvVars.py +41 -0
  22. data/legacy/VerifyBinarySigning.py +80 -0
  23. data/legacy/svn-clean.pl +246 -0
  24. data/legacy/svncopy.pl +1134 -0
  25. data/lib/nixenvironment.rb +5 -0
  26. data/lib/nixenvironment/archiver.rb +690 -0
  27. data/lib/nixenvironment/build_env_vars_loader.rb +24 -0
  28. data/lib/nixenvironment/cmd_executor.rb +33 -0
  29. data/lib/nixenvironment/config.rb +127 -14
  30. data/lib/nixenvironment/git.rb +107 -0
  31. data/lib/nixenvironment/plist.rb +52 -0
  32. data/lib/nixenvironment/version.rb +1 -1
  33. data/lib/nixenvironment/xcodebuild.rb +167 -0
  34. data/nixenvironment.gemspec +6 -0
  35. data/utils/XcodeIconTagger/IconTagger +0 -0
  36. data/utils/XcodeIconTagger/masks/OneLineMask.png +0 -0
  37. data/utils/XcodeIconTagger/masks/TwoLineMask.png +0 -0
  38. data/utils/aapt +0 -0
  39. data/utils/gcovr +986 -0
  40. data/utils/identitieslist +0 -0
  41. data/utils/simian-2.3.33.jar +0 -0
  42. metadata +118 -2
@@ -1,2 +1,7 @@
1
1
  require 'nixenvironment/version'
2
2
  require 'nixenvironment/config'
3
+ require 'nixenvironment/git'
4
+ require 'nixenvironment/xcodebuild'
5
+ require 'nixenvironment/archiver'
6
+ require 'nixenvironment/build_env_vars_loader'
7
+ require 'nixenvironment/plist'
@@ -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