|
19 | 19 | import json |
20 | 20 | import os |
21 | 21 | import os.path |
22 | | -import plistlib |
23 | | -import re |
24 | 22 | import shutil |
25 | 23 | import subprocess |
26 | 24 | import sys |
27 | 25 | import tempfile |
28 | | -from typing import Dict, List, Optional, Tuple, Union |
| 26 | +from typing import Dict, List, Optional, Union |
29 | 27 | import uuid |
30 | 28 |
|
31 | 29 | from tools.dossier_codesigningtool import dossier_codesigning_reader as dossier_reader |
|
34 | 32 | _ManifestJsonValue = Union[str, List[str], Dict[str, str]] |
35 | 33 |
|
36 | 34 |
|
37 | | -class DossierDirectory(object): |
38 | | - """Class to manage dossier directories. |
39 | | -
|
40 | | - Must used as a context manager. |
41 | | -
|
42 | | - Attributes: |
43 | | - path: The string path to the directory. |
44 | | - unzipped: A boolean indicating if the dossier was unzipped or already was a |
45 | | - directory. |
46 | | - """ |
47 | | - |
48 | | - def __init__(self, path, unzipped): |
49 | | - self.path = path |
50 | | - self.unzipped = unzipped |
51 | | - |
52 | | - def __enter__(self): |
53 | | - return self |
54 | | - |
55 | | - def __exit__(self, exception_type, exception_value, traceback): |
56 | | - if self.unzipped: |
57 | | - shutil.rmtree(self.path) |
58 | | - |
59 | | - |
60 | | -# Directories within a bundle that embedded bundles may be present in. |
61 | | -_EMBEDDED_BUNDLE_DIRECTORY_NAMES = [ |
62 | | - 'AppClips', 'Extensions', 'PlugIns', 'Frameworks', 'Watch' |
63 | | -] |
64 | | - |
65 | | - |
66 | 35 | def generate_arg_parser(): |
67 | 36 | """Generates an argument parser for this tool.""" |
68 | 37 | parser = argparse.ArgumentParser( |
69 | 38 | description='Tool for generating dossiers for signing iOS bundles.', |
70 | 39 | fromfile_prefix_chars='@') |
71 | 40 | subparsers = parser.add_subparsers(help='Sub-commands') |
72 | 41 |
|
73 | | - generate_parser = subparsers.add_parser( |
74 | | - 'generate', help='Generate a dossier from a signed bundle.') |
75 | | - generate_parser.add_argument( |
76 | | - '--output', |
77 | | - required=True, |
78 | | - help='Path to output manifest dossier location.') |
79 | | - generate_parser.add_argument( |
80 | | - '--zip', |
81 | | - action='store_true', |
82 | | - help='Zip the final dossier into a file at specified location.') |
83 | | - generate_parser.add_argument( |
84 | | - '--codesign', required=True, type=str, help='Path to codesign binary') |
85 | | - generate_parser.add_argument('bundle', help='Path to the bundle') |
86 | | - generate_parser.set_defaults(func=_generate_manifest_dossier) |
87 | | - |
88 | 42 | create_parser = subparsers.add_parser('create', help='Create a dossier.') |
89 | 43 | create_parser.add_argument( |
90 | 44 | '--output', |
@@ -127,88 +81,9 @@ def generate_arg_parser(): |
127 | 81 | ) |
128 | 82 | create_parser.set_defaults(func=_create_dossier) |
129 | 83 |
|
130 | | - embed_parser = subparsers.add_parser( |
131 | | - 'embed', |
132 | | - help=( |
133 | | - 'Embeds a dossier into an existing dossier. Only supports embedding' |
134 | | - ' at the top level of the existing dossier.' |
135 | | - ), |
136 | | - ) |
137 | | - embed_parser.add_argument( |
138 | | - '--dossier', required=True, help='Path to dossier location to edit.') |
139 | | - embed_parser.add_argument( |
140 | | - '--embedded_relative_artifact_path', |
141 | | - required=True, |
142 | | - type=str, |
143 | | - help='Relative path of artifact the dossier to be embedded signs') |
144 | | - embed_parser.add_argument( |
145 | | - '--embedded_dossier_path', |
146 | | - required=True, |
147 | | - type=str, |
148 | | - help='Path to dossier to be embedded') |
149 | | - embed_parser.set_defaults(func=_embed_dossier) |
150 | | - |
151 | 84 | return parser |
152 | 85 |
|
153 | 86 |
|
154 | | -def _extract_codesign_data( |
155 | | - bundle_path: str, |
156 | | - output_directory: str, |
157 | | - unique_id: str, |
158 | | - codesign_path: str) -> Tuple[Optional[str], Optional[str]]: |
159 | | - """Extracts codesigning data from the provided bundle to the output directory. |
160 | | -
|
161 | | - Given a bundle_path will extract the entitlements file to the provided |
162 | | - output_directory as well as extract the codesigning identity. |
163 | | -
|
164 | | - Args: |
165 | | - bundle_path: The absolute path to the bundle to extract entitlements from. |
166 | | - output_directory: The absolute path to the output directory the entitlements |
167 | | - should be placed in, it must already exist. |
168 | | - unique_id: Unique identifier to use for filename of extracted entitlements. |
169 | | - codesign_path: Path to the codesign tool as a string. |
170 | | -
|
171 | | - Returns: |
172 | | - A tuple of the output file name for the entitlements in the output_directory |
173 | | - and the codesigning identity. If either of these is not available, they will |
174 | | - be set to None in the tuple. |
175 | | -
|
176 | | - Raises: |
177 | | - OSError: If unable to extract codesign identity. |
178 | | - """ |
179 | | - command = (codesign_path, '-dvv', '--entitlements', ':-', bundle_path) |
180 | | - process = subprocess.Popen( |
181 | | - command, |
182 | | - stdout=subprocess.PIPE, |
183 | | - stderr=subprocess.PIPE, |
184 | | - encoding='utf8', |
185 | | - errors='replace') |
186 | | - output, stderr = process.communicate() |
187 | | - if process.poll() != 0: |
188 | | - raise OSError('Fail to extract entitlements from bundle: %s' % stderr) |
189 | | - |
190 | | - if not output: |
191 | | - return None, None |
192 | | - |
193 | | - signing_info = re.search(r'^Authority=(.*)$', str(stderr), re.MULTILINE) |
194 | | - if signing_info: |
195 | | - cert_authority = signing_info.group(1) |
196 | | - else: |
197 | | - cert_authority = None |
198 | | - |
199 | | - plist = plistlib.loads(output.encode('utf-8')) |
200 | | - if not plist: |
201 | | - return None, cert_authority |
202 | | - |
203 | | - output_file_name = unique_id + '.entitlements' |
204 | | - output_file_path = os.path.join(output_directory, output_file_name) |
205 | | - output_file = open(output_file_path, 'w') |
206 | | - output_file.write(output) |
207 | | - output_file.close() |
208 | | - |
209 | | - return output_file_name, cert_authority |
210 | | - |
211 | | - |
212 | 87 | def _copy_entitlements_file( |
213 | 88 | original_entitlements_file_path: str, |
214 | 89 | output_directory: str, |
@@ -261,40 +136,6 @@ def _copy_provisioning_profile( |
261 | 136 | return dest_provisioning_profile_filename |
262 | 137 |
|
263 | 138 |
|
264 | | -def _extract_provisioning_profile( |
265 | | - bundle_path: str, |
266 | | - output_directory: str, |
267 | | - unique_id: str) -> Optional[str]: |
268 | | - """Extracts the profile for the provided bundle to a destination file name. |
269 | | -
|
270 | | - Given a bundle_path will extract the profile file to the provided |
271 | | - output_directory, and return the filename relative to the output_directory |
272 | | - that the profile has been placed in, or None if no profile exists. |
273 | | -
|
274 | | - Args: |
275 | | - bundle_path: The absolute path to the bundle to extract profile from. |
276 | | - output_directory: The absolute path to the output directory the profile |
277 | | - should be placed in, it must already exist. |
278 | | - unique_id: Unique identifier to use for filename of extracted profile. |
279 | | -
|
280 | | - Returns: |
281 | | - The filename relative to output_directory the profile was placed in, |
282 | | - or None if there was no profile found. |
283 | | - """ |
284 | | - embedded_mobileprovision_path = os.path.join(bundle_path, |
285 | | - 'embedded.mobileprovision') |
286 | | - embedded_provisioning_profile_path = os.path.join( |
287 | | - bundle_path, 'Contents', 'embedded.provisionprofile') |
288 | | - if os.path.exists(embedded_mobileprovision_path): |
289 | | - original_provisioning_profile_path = embedded_mobileprovision_path |
290 | | - elif os.path.exists(embedded_provisioning_profile_path): |
291 | | - original_provisioning_profile_path = embedded_provisioning_profile_path |
292 | | - else: |
293 | | - return None |
294 | | - return _copy_provisioning_profile(original_provisioning_profile_path, |
295 | | - output_directory, unique_id) |
296 | | - |
297 | | - |
298 | 139 | def _generate_manifest( |
299 | 140 | codesign_identity: Optional[str], |
300 | 141 | entitlement_file: Optional[str], |
@@ -335,115 +176,6 @@ def _generate_manifest( |
335 | 176 | .EMBEDDED_BUNDLE_MANIFESTS_KEY] = embedded_bundle_manifests |
336 | 177 | return manifest |
337 | 178 |
|
338 | | - |
339 | | -def _embedded_manifests_for_path( |
340 | | - bundle_path: str, |
341 | | - dossier_directory: str, |
342 | | - target_directory: str, |
343 | | - codesign_path: str, |
344 | | -) -> List[Dict[str, _ManifestJsonValue]]: |
345 | | - """Generates embedded manifests for a bundle in a sub-directory. |
346 | | -
|
347 | | - Provided a bundle, output directory, and a target directory, traverses the |
348 | | - target directory to find any bundles that are signed, and generate manifests. |
349 | | - Copies any referenced assets to the output directory. |
350 | | -
|
351 | | - Args: |
352 | | - bundle_path: The absolute path to the bundle that will be searched. |
353 | | - dossier_directory: The absolute path to the output dossier directory that |
354 | | - manifest referenced assets will be copied to. |
355 | | - target_directory: The target directory name, relative to the bundle_path, to |
356 | | - be traversed. |
357 | | - codesign_path: Path to the codesign tool as a string. |
358 | | -
|
359 | | - Returns: |
360 | | - A list of manifest contents with the contents they reference copied into |
361 | | - dossier_directory, or an empty list if no bundles are codesigned. |
362 | | - """ |
363 | | - if target_directory not in _EMBEDDED_BUNDLE_DIRECTORY_NAMES: |
364 | | - raise ValueError( |
365 | | - 'Invalid bundle directory for dossier manifest: %s' % target_directory) |
366 | | - |
367 | | - embedded_manifests = [] |
368 | | - target_directory_path = os.path.join(bundle_path, target_directory) |
369 | | - if os.path.exists(target_directory_path): |
370 | | - target_directory_contents = os.listdir(target_directory_path) |
371 | | - target_directory_contents.sort() |
372 | | - for filename in target_directory_contents: |
373 | | - absolute_embedded_bundle_path = os.path.join(target_directory_path, |
374 | | - filename) |
375 | | - embedded_manifest = _manifest_with_dossier_for_bundle( |
376 | | - absolute_embedded_bundle_path, dossier_directory, codesign_path) |
377 | | - if embedded_manifest is not None: |
378 | | - embedded_manifest[ |
379 | | - dossier_reader.EMBEDDED_RELATIVE_PATH_KEY] = os.path.join( |
380 | | - target_directory, filename) |
381 | | - embedded_manifests.append(embedded_manifest) |
382 | | - return embedded_manifests |
383 | | - |
384 | | - |
385 | | -def _manifest_with_dossier_for_bundle( |
386 | | - bundle_path: str, |
387 | | - dossier_directory: str, |
388 | | - codesign_path: str) -> Optional[Dict[str, _ManifestJsonValue]]: |
389 | | - """Generates a manifest and assets for a provided bundle. |
390 | | -
|
391 | | - Provided a bundle and output directory, prepares a code signing dossier by |
392 | | - generating the manifest contents for the bundle referenced and copying any |
393 | | - assets referenced by the manifest into the dossier folder. |
394 | | -
|
395 | | - Args: |
396 | | - bundle_path: The absolute path to the bundle that a manifest will be |
397 | | - generated for. |
398 | | - dossier_directory: The absolute path to the output dossier directory that |
399 | | - manifest referenced assets will be copied to. |
400 | | - codesign_path: Path to the codesign tool as a string. |
401 | | -
|
402 | | - Returns: |
403 | | - The manifest contents with files they reference copied into |
404 | | - dossier_directory. |
405 | | - """ |
406 | | - unique_id = str(uuid.uuid4()) |
407 | | - entitlements_file, codesign_identity = _extract_codesign_data( |
408 | | - bundle_path, dossier_directory, unique_id, codesign_path) |
409 | | - if not codesign_identity: |
410 | | - return None |
411 | | - provisioning_profile = _extract_provisioning_profile(bundle_path, |
412 | | - dossier_directory, |
413 | | - unique_id) |
414 | | - embedded_manifests = [] |
415 | | - for embedded_bundle_directory in _EMBEDDED_BUNDLE_DIRECTORY_NAMES: |
416 | | - embedded_manifests.extend( |
417 | | - _embedded_manifests_for_path(bundle_path, dossier_directory, |
418 | | - embedded_bundle_directory, codesign_path)) |
419 | | - if not embedded_manifests: |
420 | | - embedded_manifests = None |
421 | | - return _generate_manifest(codesign_identity, entitlements_file, |
422 | | - provisioning_profile, embedded_manifests) |
423 | | - |
424 | | - |
425 | | -def _generate_manifest_dossier(parsed_args: argparse.Namespace): |
426 | | - """Generates a manifest dossier for provided args.""" |
427 | | - bundle_path = parsed_args.bundle |
428 | | - dossier_directory = parsed_args.output |
429 | | - |
430 | | - packaging_required = False |
431 | | - if parsed_args.zip: |
432 | | - dossier_directory = tempfile.mkdtemp() |
433 | | - packaging_required = True |
434 | | - |
435 | | - if not os.path.exists(dossier_directory): |
436 | | - os.makedirs(dossier_directory) |
437 | | - |
438 | | - manifest = _manifest_with_dossier_for_bundle( |
439 | | - os.path.abspath(bundle_path), dossier_directory, parsed_args.codesign) |
440 | | - |
441 | | - _write_manifest(manifest, dossier_directory) |
442 | | - if packaging_required: |
443 | | - _zip_dossier(dossier_directory, parsed_args.output) |
444 | | - shutil.rmtree(dossier_directory) |
445 | | - |
446 | | - |
447 | 179 | def _write_manifest( |
448 | 180 | manifest: Dict[str, _ManifestJsonValue], |
449 | 181 | dossier_directory: str) -> None: |
@@ -567,40 +299,6 @@ def _create_dossier(parsed_args: argparse.Namespace): |
567 | 299 | shutil.rmtree(dossier_directory) |
568 | 300 |
|
569 | 301 |
|
570 | | -def _embed_dossier(parsed_args): |
571 | | - """Embeds an existing dossier into the specified dossier. |
572 | | -
|
573 | | - Provided a set of args from generate sub-command, embeds a dossier in a |
574 | | - dossier. |
575 | | -
|
576 | | - Args: |
577 | | - parsed_args: A struct of arguments required for generating a dossier from a |
578 | | - signed bundle that were generated from an instance of |
579 | | - argparse.ArgumentParser(...). |
580 | | -
|
581 | | - Raises: |
582 | | - OSError: If any of specified dossiers are not found. |
583 | | - """ |
584 | | - with (dossier_reader.extract_zipped_dossier_if_required( |
585 | | - parsed_args.dossier) as dossier_dir, |
586 | | - dossier_reader.extract_zipped_dossier_if_required( |
587 | | - parsed_args.embedded_dossier_path) as embedded_dossier_dir): |
588 | | - |
589 | | - manifest = dossier_reader.read_manifest_from_dossier(dossier_dir.path) |
590 | | - embedded_manifest = dossier_reader.read_manifest_from_dossier( |
591 | | - embedded_dossier_dir.path) |
592 | | - |
593 | | - _merge_dossier_contents(embedded_dossier_dir.path, dossier_dir.path) |
594 | | - embedded_manifest[dossier_reader.EMBEDDED_RELATIVE_PATH_KEY] = ( |
595 | | - parsed_args.embedded_relative_artifact_path) |
596 | | - manifest[dossier_reader.EMBEDDED_BUNDLE_MANIFESTS_KEY].append( |
597 | | - embedded_manifest) |
598 | | - |
599 | | - _write_manifest(manifest, dossier_dir.path) |
600 | | - if dossier_dir.unzipped: |
601 | | - _zip_dossier(dossier_dir.path, parsed_args.dossier) |
602 | | - |
603 | | - |
604 | 302 | if __name__ == '__main__': |
605 | 303 | args = generate_arg_parser().parse_args() |
606 | 304 | sys.exit(args.func(args)) |
0 commit comments