# Copyright 2018 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Creates a archive manifest used for Fuchsia package generation. Arguments: root_dir: The absolute path to the Chromium source tree root. out_dir: The absolute path to the Chromium build directory. app_name: The filename of the package's executable target. runtime_deps: The path to the GN runtime deps file. output_path: The path of the manifest file which will be written. """ import json import os import re import subprocess import sys import tempfile def ReadDynamicLibDeps(paths): """Returns a list of NEEDED libraries read from a binary's ELF header.""" LIBRARY_RE = re.compile(r'.*\(NEEDED\)\s+Shared library: \[(?P.*)\]') elfinfo = subprocess.check_output(['readelf', '-d'] + paths, stderr=open(os.devnull, 'w')) libs = [] for line in elfinfo.split('\n'): match = LIBRARY_RE.match(line.rstrip()) if match: lib = match.group('lib') # Skip libzircon.so, as it is supplied by the OS loader. if lib != 'libzircon.so': libs.append(match.group('lib')) return libs def ComputeTransitiveLibDeps(executable_path, available_libs): """Returns a set representing the library dependencies of |executable_path|, the dependencies of its dependencies, and so on. A list of candidate library filesystem paths is passed using |available_libs| to help with resolving full paths from the short ELF header filenames.""" # Stack of binaries (libraries, executables) awaiting traversal. to_visit = [executable_path] # The computed set of visited transitive dependencies. deps = set() while to_visit: deps = deps.union(to_visit) # Resolve the full paths for all of |cur_path|'s NEEDED libraries. dep_paths = {available_libs[dep] for dep in ReadDynamicLibDeps(list(to_visit))} # Add newly discovered dependencies to the pending traversal stack. to_visit = dep_paths.difference(deps) return deps def EnumerateDirectoryFiles(path): """Returns a flattened list of all files contained under |path|.""" output = set() for dirname, _, files in os.walk(path): output = output.union({os.path.join(dirname, f) for f in files}) return output def MakePackagePath(file_path, roots): """Computes a path for |file_path| that is relative to one of the directory paths in |roots|. file_path: The absolute file path to relativize. roots: A list of absolute directory paths which may serve as a relative root for |file_path|. At least one path must contain |file_path|. Overlapping roots are permitted; the deepest matching root will be chosen. Examples: >>> MakePackagePath('/foo/bar.txt', ['/foo/']) 'bar.txt' >>> MakePackagePath('/foo/dir/bar.txt', ['/foo/']) 'dir/bar.txt' >>> MakePackagePath('/foo/out/Debug/bar.exe', ['/foo/', '/foo/out/Debug/']) 'bar.exe' """ # Prevents greedily matching against a shallow path when a deeper, better # matching path exists. roots.sort(key=len, reverse=True) for next_root in roots: if not next_root.endswith(os.sep): next_root += os.sep if file_path.startswith(next_root): relative_path = file_path[len(next_root):] # Move all dynamic libraries (ending in .so or .so.) to lib/. if re.search('.*\.so(\.\d+)?$', file_path): relative_path = 'lib/' + os.path.basename(relative_path) return relative_path raise Exception('Error: no matching root paths found for \'%s\'.' % file_path) def _GetStrippedPath(bin_path): """Finds the stripped version of the binary |bin_path| in the build output directory.""" # Skip the resolution step for binaries that don't have stripped counterparts, # like system libraries or other libraries built outside the Chromium build. if not '.unstripped' in bin_path: return bin_path return os.path.normpath(os.path.join(bin_path, os.path.pardir, os.path.pardir, os.path.basename(bin_path))) def _IsBinary(path): """Checks if the file at |path| is an ELF executable by inspecting its FourCC header.""" with open(path, 'rb') as f: file_tag = f.read(4) return file_tag == '\x7fELF' def BuildManifest(root_dir, out_dir, app_name, app_filename, sandbox_policy_path, runtime_deps_file, depfile_path, dynlib_paths, output_path): with open(output_path, 'w') as manifest, open(depfile_path, 'w') as depfile: # Process the runtime deps file for file paths, recursively walking # directories as needed. File paths are stored in absolute form, # so that MakePackagePath() may relativize to either the source root or # output directory. # runtime_deps may contain duplicate paths, so use a set for # de-duplication. expanded_files = set() for next_path in open(runtime_deps_file, 'r'): next_path = next_path.strip() if os.path.isdir(next_path): for root, _, files in os.walk(next_path): for current_file in files: if current_file.startswith('.'): continue expanded_files.add(os.path.abspath( os.path.join(root, current_file))) else: expanded_files.add(os.path.abspath(next_path)) # Get set of dist libraries available for dynamic linking. dist_libs = set() for next_dir in dynlib_paths.split(','): dist_libs = dist_libs.union(EnumerateDirectoryFiles(next_dir)) # Compute the set of dynamic libraries used by the application or its # transitive dependencies (dist libs and components), and merge the result # with |expanded_files| so that they are included in the manifest. expanded_files = expanded_files.union( ComputeTransitiveLibDeps( app_filename, {os.path.basename(f): f for f in expanded_files.union(dist_libs)})) # Format and write out the manifest contents. app_found = False for current_file in expanded_files: if _IsBinary(current_file): current_file = _GetStrippedPath(current_file) in_package_path = MakePackagePath(os.path.join(out_dir, current_file), [root_dir, out_dir]) if in_package_path == app_filename: in_package_path = 'bin/app' app_found = True # The source path is relativized so that it can be used on multiple # environments with differing parent directory structures, # e.g. builder bots and swarming clients. manifest.write('%s=%s\n' % (in_package_path, os.path.relpath(current_file, out_dir))) # Use libc.so's dynamic linker by aliasing libc.so to ld.so.1. # Fuchsia always looks for the linker implementation in ld.so.1. if os.path.basename(in_package_path) == 'libc.so': manifest.write( '%s=%s\n' % (os.path.dirname(in_package_path) + '/ld.so.1', os.path.relpath(current_file, out_dir))) if not app_found: raise Exception('Could not locate executable inside runtime_deps.') with open(os.path.join(os.path.dirname(output_path), 'package'), 'w') \ as package_json: json.dump({'version': '0', 'name': app_name}, package_json) manifest.write('meta/package=%s\n' % os.path.relpath(package_json.name, out_dir)) manifest.write('meta/sandbox=%s\n' % os.path.relpath(os.path.join(root_dir, sandbox_policy_path), out_dir)) depfile.write( "%s: %s" % (os.path.relpath(output_path, out_dir), " ".join([os.path.relpath(f, out_dir) for f in expanded_files]))) return 0 if __name__ == '__main__': sys.exit(BuildManifest(*sys.argv[1:]))