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