#!/usr/bin/env python3 # # Copyright 2013 The Flutter Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import os import platform import shutil import subprocess import sys def assert_directory(path, what): """Logs an error and exits with EX_NOINPUT if the specified directory doesn't exist.""" if not os.path.isdir(path): log_error('Cannot find %s at %s' % (what, path)) sys.exit(os.EX_NOINPUT) def assert_file(path, what): """Logs an error and exits with EX_NOINPUT if the specified file doesn't exist.""" if not os.path.isfile(path): log_error('Cannot find %s at %s' % (what, path)) sys.exit(os.EX_NOINPUT) def assert_valid_codesign_config( framework_dir, zip_contents, entitlements, without_entitlements, unsigned_binaries ): """Exits with exit code 1 if the codesign configuration contents are incorrect. All Mach-O binaries found within zip_contents exactly must be listed in either entitlements or without_entitlements.""" if _contains_duplicates(entitlements): log_error('ERROR: duplicate value(s) found in entitlements.txt') log_error_items(sorted(entitlements)) sys.exit(os.EX_DATAERR) if _contains_duplicates(without_entitlements): log_error('ERROR: duplicate value(s) found in without_entitlements.txt') log_error_items(sorted(without_entitlements)) sys.exit(os.EX_DATAERR) if _contains_duplicates(unsigned_binaries): log_error('ERROR: duplicate value(s) found in unsigned_binaries.txt') log_error_items(sorted(unsigned_binaries)) sys.exit(os.EX_DATAERR) if _contains_duplicates(entitlements + without_entitlements + unsigned_binaries): log_error( 'ERROR: duplicate value(s) found between ' 'entitlements.txt, without_entitlements.txt, unsigned_binaries.txt' ) log_error_items(sorted(entitlements + without_entitlements + unsigned_binaries)) sys.exit(os.EX_DATAERR) binaries = set() for zip_content_path in zip_contents: # If file, check if Mach-O binary. if _is_macho_binary(os.path.join(framework_dir, zip_content_path)): binaries.add(zip_content_path) # If directory, check transitive closure of files for Mach-O binaries. for root, _, files in os.walk(os.path.join(framework_dir, zip_content_path)): for file in [os.path.join(root, f) for f in files]: if _is_macho_binary(file): binaries.add(os.path.relpath(file, framework_dir)) # Verify that all Mach-O binaries are listed in either entitlements, # without_entitlements, or unsigned_binaries. listed_binaries = set(entitlements + without_entitlements + unsigned_binaries) if listed_binaries != binaries: log_error( 'ERROR: binaries listed in entitlements.txt, without_entitlements.txt, and' 'unsigned_binaries.txt do not match the set of binaries in the files to be zipped' ) log_error('Binaries found in files to be zipped:') for file in sorted(binaries): log_error(' ' + file) not_listed = sorted(binaries - listed_binaries) if not_listed: log_error( 'Binaries NOT LISTED in entitlements.txt, without_entitlements.txt, ' 'unsigned_binaries.txt:' ) for file in not_listed: log_error(' ' + file) not_found = sorted(listed_binaries - binaries) if not_found: log_error( 'Binaries listed in entitlements.txt, without_entitlements.txt, ' 'unsigned_binaries.txt but NOT FOUND:' ) for file in not_found: log_error(' ' + file) sys.exit(os.EX_NOINPUT) def _contains_duplicates(strings): """Returns true if the list of strings contains a duplicate value.""" return len(strings) != len(set(strings)) def _is_macho_binary(filename): """Returns True if the specified path is file and a Mach-O binary.""" if os.path.islink(filename) or not os.path.isfile(filename): return False with open(filename, 'rb') as file: chunk = file.read(4) return chunk in ( b'\xca\xfe\xba\xbe', # Mach-O Universal Big Endian b'\xce\xfa\xed\xfe', # Mach-O Little Endian (32-bit) b'\xcf\xfa\xed\xfe', # Mach-O Little Endian (64-bit) b'\xfe\xed\xfa\xce', # Mach-O Big Endian (32-bit) b'\xfe\xed\xfa\xcf', # Mach-O Big Endian (64-bit) ) def buildroot_relative_path(path): """Returns the absolute path to the specified buildroot-relative path.""" buildroot_dir = os.path.abspath(os.path.join(os.path.realpath(__file__), '..', '..', '..', '..')) return os.path.join(buildroot_dir, path) def copy_binary(source_path, destination_path): """Copies a binary, preserving POSIX permissions.""" assert_file(source_path, 'file to copy') shutil.copy2(source_path, destination_path) def copy_tree(source_path, destination_path, symlinks=False): """Performs a recursive copy of a directory. If the destination path is present, it is deleted first.""" assert_directory(source_path, 'directory to copy') shutil.rmtree(destination_path, True) shutil.copytree(source_path, destination_path, symlinks=symlinks) def create_fat_macos_framework(args, dst, fat_framework, arm64_framework, x64_framework): """Creates a fat framework from two arm64 and x64 frameworks.""" # Clone the arm64 framework bundle as a starting point. copy_tree(arm64_framework, fat_framework, symlinks=True) _regenerate_symlinks(fat_framework) framework_dylib = get_mac_framework_dylib_path(fat_framework) lipo([get_mac_framework_dylib_path(arm64_framework), get_mac_framework_dylib_path(x64_framework)], framework_dylib) _set_framework_permissions(fat_framework) framework_dsym = fat_framework + '.dSYM' if args.dsym else None _process_macos_framework(args, dst, framework_dylib, framework_dsym) def _regenerate_symlinks(framework_dir): """Regenerates the framework symlink structure. When building on the bots, the framework is produced in one shard, uploaded to LUCI's content-addressable storage cache (CAS), then pulled down in another shard. When that happens, symlinks are dereferenced, resulting a corrupted framework. This regenerates the expected symlink farm. """ # If the dylib is symlinked, assume symlinks are all fine and bail out. # The shutil.rmtree calls below only work on directories, and fail on symlinks. framework_name = get_framework_name(framework_dir) if os.path.islink(os.path.join(framework_dir, framework_name)): return # Delete any existing files/directories. os.remove(os.path.join(framework_dir, framework_name)) shutil.rmtree(os.path.join(framework_dir, 'Headers'), True) shutil.rmtree(os.path.join(framework_dir, 'Modules'), True) shutil.rmtree(os.path.join(framework_dir, 'Resources'), True) current_version_path = os.path.join(framework_dir, 'Versions', 'Current') shutil.rmtree(current_version_path, True) # Recreate the expected framework symlinks. os.symlink('A', current_version_path) os.symlink( os.path.join('Versions', 'Current', framework_name), os.path.join(framework_dir, framework_name) ) os.symlink(os.path.join('Versions', 'Current', 'Headers'), os.path.join(framework_dir, 'Headers')) os.symlink(os.path.join('Versions', 'Current', 'Modules'), os.path.join(framework_dir, 'Modules')) os.symlink( os.path.join('Versions', 'Current', 'Resources'), os.path.join(framework_dir, 'Resources') ) def _set_framework_permissions(framework_dir): """Sets framework contents to be world readable, and world executable if user-executable.""" # Make the framework readable and executable: u=rwx,go=rx. subprocess.check_call(['chmod', '755', framework_dir]) # Add group and other readability to all files. versions_path = os.path.join(framework_dir, 'Versions') subprocess.check_call(['chmod', '-R', 'og+r', versions_path]) # Find all the files below the target dir with owner execute permission find_subprocess = subprocess.Popen(['find', versions_path, '-perm', '-100', '-print0'], stdout=subprocess.PIPE) # Add execute permission for other and group for all files that had it for owner. xargs_subprocess = subprocess.Popen(['xargs', '-0', 'chmod', 'og+x'], stdin=find_subprocess.stdout) find_subprocess.wait() xargs_subprocess.wait() def _process_macos_framework(args, dst, framework_dylib, dsym): if dsym: extract_dsym(framework_dylib, dsym) if args.strip: unstripped_out = os.path.join(dst, 'FlutterMacOS.unstripped') strip_binary(framework_dylib, unstripped_out) def create_zip(cwd, zip_filename, paths): """Creates a zip archive in cwd, containing a set of cwd-relative files. In order to preserve the correct internal structure of macOS frameworks, symlinks are preserved (-y). In order to generate reproducible builds, owner/group and unix file timestamps are not included in the archive (-X). """ subprocess.check_call(['zip', '-r', '-X', '-y', zip_filename] + paths, cwd=cwd) def _dsymutil_path(): """Returns the path to dsymutil within Flutter's clang toolchain.""" arch_subpath = 'mac-arm64' if platform.processor() == 'arm' else 'mac-x64' dsymutil_path = os.path.join('flutter', 'buildtools', arch_subpath, 'clang', 'bin', 'dsymutil') return buildroot_relative_path(dsymutil_path) def get_framework_name(framework_dir): """Returns Foo given /path/to/Foo.framework.""" return os.path.splitext(os.path.basename(framework_dir))[0] def get_mac_framework_dylib_path(framework_dir): """Returns /path/to/Foo.framework/Versions/A/Foo given /path/to/Foo.framework.""" return os.path.join(framework_dir, 'Versions', 'A', get_framework_name(framework_dir)) def extract_dsym(binary_path, dsym_out_path): """Extracts a dSYM bundle from the specified Mach-O binary.""" arch_dir = 'mac-arm64' if platform.processor() == 'arm' else 'mac-x64' dsymutil = buildroot_relative_path( os.path.join('flutter', 'buildtools', arch_dir, 'clang', 'bin', 'dsymutil') ) subprocess.check_call([dsymutil, '-o', dsym_out_path, binary_path]) def lipo(input_binaries, output_binary): """Uses lipo to create a fat binary from a set of input binaries.""" subprocess.check_call(['lipo'] + input_binaries + ['-create', '-output', output_binary]) def log_error(message): """Writes the message to stderr, followed by a newline.""" print(message, file=sys.stderr) def log_error_items(items): """Writes each item indented to stderr, followed by a newline.""" for item in items: log_error(' ' + item) def strip_binary(binary_path, unstripped_copy_path): """Makes a copy of an unstripped binary, then strips symbols from the binary.""" assert_file(binary_path, 'binary to strip') shutil.copyfile(binary_path, unstripped_copy_path) subprocess.check_call(['strip', '-x', '-S', binary_path]) def write_codesign_config(output_path, paths): """Writes an Apple codesign configuration file containing the specified paths.""" with open(output_path, mode='w', encoding='utf-8') as file: if paths: file.write('\n'.join(paths) + '\n')