|  | #!/usr/bin/env python2.7 | 
|  | # Copyright 2019 The Fuchsia 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 argparse | 
|  | import datetime | 
|  | import json | 
|  | import logging | 
|  | import os | 
|  | import re | 
|  | import subprocess | 
|  | import sys | 
|  | import tempfile | 
|  |  | 
|  | tool_description = """Move directory to new location in source tree | 
|  |  | 
|  |  | 
|  | This is a tool to move a directory of source code from one place in the | 
|  | Fuchsia Platform Source Tree to another location in a minimally invasive | 
|  | manner.  This script attempts to identify references to the original location | 
|  | and updates them or generates artifacts that transparently forward to the | 
|  | new location.  To use: | 
|  |  | 
|  | 1.) Check out origin/master and configure a build in the default build | 
|  | directory with 'fx set' | 
|  | 2.) Run 'scripts/move/source/move_source.py <source> <dest>'. This will | 
|  | generate a new git branch and create a commit with a description of what the | 
|  | tool did. | 
|  | 3.) Examine the references that the tool reports that it could not handle | 
|  | and fix up as needed with 'git commit -a' | 
|  | 4.) Upload for review | 
|  |  | 
|  | """ | 
|  |  | 
|  | fuchsia_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) | 
|  |  | 
|  |  | 
|  | """ | 
|  | Creates a git branch for a move and checks it out. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def create_branch_for_move(source, dest, dry_run): | 
|  | branch_name = 'move_%s_to_%s' % (source.replace('/', '_'), | 
|  | dest.replace('/', '_')) | 
|  |  | 
|  | run_command(['git', 'checkout', '-b', branch_name, '--track', | 
|  | 'origin/master'], dry_run) | 
|  |  | 
|  | """ | 
|  | Guess if a target is a go library target, which requires a specialized | 
|  | forwarding target type. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def is_go_library_target(target): | 
|  | return (target['type'] == 'action' and | 
|  | target['script'] == '//build/go/gen_library_metadata.py') | 
|  |  | 
|  |  | 
|  | """ | 
|  | Stores information about a forwarding GN target to generate. | 
|  | """ | 
|  |  | 
|  |  | 
|  | class ForwardingTarget: | 
|  |  | 
|  | def __init__(self, label, target): | 
|  | self.label = label | 
|  | self.testonly = target['testonly'] | 
|  | self.is_go_library = is_go_library_target(target) | 
|  |  | 
|  | def __repr__(self): | 
|  | return 'ForwardingTarget(%s, %s, %s)' % (self.label, self.testonly, | 
|  | self.is_go_library) | 
|  |  | 
|  | def __str__(self): | 
|  | return '%s testonly %s go_library %s' % ( | 
|  | self.label, self.testonly, self.is_go_library) | 
|  |  | 
|  |  | 
|  | """ | 
|  | Finds all external references to GN targets in the source directory and | 
|  | records information needed to generate forwarding targets. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def find_referenced_targets(build_graph, source): | 
|  | # List of targets in the source directory tree. Each entry is a tuple | 
|  | # of (string, boolean) | 
|  | targets_in_source = [] | 
|  |  | 
|  | # Set of targets (identified by gn label) in the source directory that | 
|  | # are referenced by targets outside the source directory. | 
|  | referenced_targets = set() | 
|  |  | 
|  | for label, target in build_graph.items(): | 
|  | if label.startswith('//' + source): | 
|  | targets_in_source.append(ForwardingTarget(label, target)) | 
|  | else: | 
|  | all_deps = target['deps'] | 
|  | if 'public_deps' in target: | 
|  | all_deps = all_deps + target['public_deps'] | 
|  | for dep in all_deps: | 
|  | # Remove the toolchain qualifier, if present | 
|  | if '(' in dep: | 
|  | dep = dep[0:dep.find('(')] | 
|  | if dep.startswith('//' + source): | 
|  | referenced_targets.add(dep) | 
|  |  | 
|  | logging.debug('targets_in_source %s' % targets_in_source) | 
|  | logging.debug('referenced_targets %s' % referenced_targets) | 
|  |  | 
|  | forwarding_targets = [] | 
|  | # compute forwarding targets to create | 
|  | for target in sorted(targets_in_source): | 
|  | if target.label in referenced_targets: | 
|  | logging.debug( | 
|  | 'Need to generate forwarding target for %s' % target) | 
|  | forwarding_targets.append(target) | 
|  |  | 
|  | return forwarding_targets | 
|  |  | 
|  |  | 
|  | """ | 
|  | Finds all references to the source path that we don't know how to automatically | 
|  | handle so the user can examine and update as needed. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def find_unknown_references(source): | 
|  | unknown_references = [] | 
|  | jiri_grep_args = ['jiri', 'grep', source] | 
|  | logging.debug('Running %s' % jiri_grep_args) | 
|  | grep_results = subprocess.check_output(jiri_grep_args, cwd=fuchsia_root) | 
|  | for line in grep_results.splitlines(): | 
|  | file, match = line.split(':', 1) | 
|  | if not os.path.normpath(file).startswith(source): | 
|  | if '#include' in match: | 
|  | continue | 
|  | if file.endswith('BUILD.gn'): | 
|  | continue | 
|  | unknown_references.append(line) | 
|  | return unknown_references | 
|  |  | 
|  |  | 
|  | """ | 
|  | Partial information about the contents of a build file. Can be combined with | 
|  | other instances and written out to a file object. | 
|  | """ | 
|  |  | 
|  |  | 
|  | class PartialBuildFile(): | 
|  |  | 
|  | def __init__(self): | 
|  | self.imports = set() | 
|  | self.snippet = '' | 
|  |  | 
|  | def merge(self, other): | 
|  | self.imports |= other.imports | 
|  | self.snippet += '\n' + other.snippet | 
|  |  | 
|  | def write(self, f): | 
|  | for import_path in sorted(self.imports): | 
|  | f.write(''' | 
|  | import("%s")''' % import_path) | 
|  | f.write(self.snippet) | 
|  |  | 
|  |  | 
|  | """ | 
|  | Generates a partial build file representing a forwarding target. | 
|  | Returns: | 
|  |  | 
|  | - path to BUILD.gn file | 
|  | - partial GN build file contents | 
|  | """ | 
|  |  | 
|  |  | 
|  | def generate_forwarding_target(target, source, dest): | 
|  | label_path, target_name = target.label.split(':') | 
|  | containing_directory = label_path[2:] | 
|  | build_file_path = os.path.join( | 
|  | fuchsia_root, containing_directory, 'BUILD.gn') | 
|  | imports = set() | 
|  | # compute label relative to directory. This will be the same in the | 
|  | # source and destination | 
|  | relative_label = os.path.relpath(containing_directory, source) | 
|  |  | 
|  | dest_path = os.path.normpath(os.path.join(dest, relative_label)) | 
|  | dest_label = '//%s:%s' % (dest_path, target_name) | 
|  |  | 
|  | logging.debug('relative_label %s dest_label %s target_name %s' % ( | 
|  | relative_label, dest_label, target_name)) | 
|  |  | 
|  | build = PartialBuildFile() | 
|  |  | 
|  | build.snippet = ''' | 
|  | # Do not use this target directly, instead depend on %s.''' % dest_label | 
|  |  | 
|  | if target.is_go_library: | 
|  | build.imports.add('//build/go/go_library.gni') | 
|  | build.snippet += ''' | 
|  | go_library("%s") { | 
|  | name = "%s_forwarding_target" | 
|  |  | 
|  | deps = [ | 
|  | ''' % (target_name, target_name) | 
|  |  | 
|  | else: | 
|  | build.snippet += ''' | 
|  | group("%s") { | 
|  | public_deps = [ | 
|  | ''' % target_name | 
|  |  | 
|  | build.snippet += '''    "%s" | 
|  | ] | 
|  | ''' % dest_label | 
|  |  | 
|  | if target.testonly: | 
|  | build.snippet += '''  testonly = true | 
|  | ''' | 
|  |  | 
|  | build.snippet += '''} | 
|  | ''' | 
|  |  | 
|  | return build_file_path, build | 
|  |  | 
|  |  | 
|  | """ | 
|  | Write out forwarding build rules for a set of forwarding targets. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def write_forwarding_build_rules(forwarding_targets, source, dest, dry_run): | 
|  | build_files = {} | 
|  | for target in forwarding_targets: | 
|  | path, build = generate_forwarding_target(target, source, dest) | 
|  | if path not in build_files: | 
|  | build_files[path] = PartialBuildFile() | 
|  |  | 
|  | build_files[path].merge(build) | 
|  |  | 
|  | for path, build in build_files.iteritems(): | 
|  | if not dry_run: | 
|  | if not os.path.exists(path): | 
|  | dest_dir = os.path.dirname(path) | 
|  | if not os.path.exists(dest_dir): | 
|  | os.makedirs(dest_dir) | 
|  | with open(path, 'w') as f: | 
|  | write_copyright_header(f, '#') | 
|  | with open(path, 'a') as f: | 
|  | build.write(f) | 
|  | run_command(['git', 'add', path], dry_run) | 
|  |  | 
|  |  | 
|  | """ | 
|  | Appends contents to a BUILD.gn file in a given directory. Creates the file | 
|  | if necessary, including copyright headers. Does not format the file it | 
|  | creates or modifies. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def append_to_gn_file(directory, contents): | 
|  | dest_path = os.path.join(fuchsia_root, directory, 'BUILD.gn') | 
|  | if not os.path.exists(dest_path): | 
|  | dest_dir = os.path.dirname(dest_path) | 
|  | if not os.path.exists(dest_dir): | 
|  | os.makedirs(dest_dir) | 
|  | with open(dest_path, 'w') as f: | 
|  | write_copyright_header(f, '#') | 
|  | with open(dest_path, 'a') as f: | 
|  | f.write(contents) | 
|  | dry_run = False | 
|  | run_command(['git', 'add', dest_path], False) | 
|  |  | 
|  |  | 
|  | """ | 
|  | Writes a copyright header for the current year into a provided open file. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def write_copyright_header(f, comment): | 
|  | copyright_header = '''%s Copyright %s The Fuchsia Authors. All rights reserved. | 
|  | %s Use of this source code is governed by a BSD-style license that can be | 
|  | %s found in the LICENSE file. | 
|  |  | 
|  | ''' % (comment, datetime.date.today().year, comment, | 
|  | comment) | 
|  | f.write(copyright_header) | 
|  |  | 
|  |  | 
|  | """ | 
|  | Generates a forwarding header. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def generate_forwarding_header(header, source, dest, dry_run): | 
|  | header_dir = os.path.dirname(header) | 
|  | relative_path = os.path.normpath(os.path.relpath(header, source)) | 
|  | dest_path = os.path.join(dest, relative_path) | 
|  |  | 
|  | logging.debug('Generating forwarding header %s pointing to %s' % ( | 
|  | header, dest_path)) | 
|  |  | 
|  | if dry_run: | 
|  | return | 
|  |  | 
|  | if not os.path.exists(header_dir): | 
|  | os.makedirs(header_dir) | 
|  | with open(header, 'w') as f: | 
|  | write_copyright_header(f, '//') | 
|  | # Formatter will generate a proper header guard | 
|  | f.write('#pragma once\n') | 
|  |  | 
|  | f.write(''' | 
|  | // Do not use this header directly, instead use %s. | 
|  |  | 
|  | ''' % dest_path) | 
|  | f.write('#include "%s"\n' % dest_path) | 
|  |  | 
|  | run_command(['git', 'add', header], dry_run) | 
|  |  | 
|  |  | 
|  | """ | 
|  | Generates a commit message for the move including general information | 
|  | about the move as well as a list of generated forwarding artifacts. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def generate_commit_message(source, dest, forwarding_targets, | 
|  | forwarding_headers, change_id): | 
|  | commit_message = '''[%s] Move %s to %s | 
|  |  | 
|  | This moves the contents of the directory: | 
|  | %s | 
|  | to the directory: | 
|  | %s | 
|  |  | 
|  | to better match Fuchsia's desired source layout: | 
|  | https://fuchsia.googlesource.com/fuchsia/+/HEAD/docs/development/source_code/layout.md | 
|  |  | 
|  | ''' % (os.path.basename(dest), source, dest, source, dest) | 
|  |  | 
|  | if len(forwarding_targets): | 
|  | commit_message += ''' | 
|  | This commit includes the following forwarding build targets to ease the | 
|  | transition to the new layout: | 
|  |  | 
|  | ''' | 
|  | for target in sorted(forwarding_targets): | 
|  | commit_message += '  %s\n' % target.label | 
|  | commit_message += ''' | 
|  | New code should rely on the new paths for these targets. A follow up commit | 
|  | will remove the forwarding targets once all references are updated. | 
|  | ''' | 
|  |  | 
|  | if len(forwarding_headers): | 
|  | commit_message += ''' | 
|  | This commit includes the following forwarding headers to ease the | 
|  | transition to the new layout: | 
|  |  | 
|  | ''' | 
|  | for header in sorted(forwarding_headers): | 
|  | commit_message += '  %s\n' % header | 
|  | commit_message += ''' | 
|  |  | 
|  | New code should rely on the new paths for these headers. A follow up commit | 
|  | will remove the forwarding headers once all references are updated. | 
|  | ''' | 
|  |  | 
|  | commit_message += ''' | 
|  | This commit is generated by the script //scripts/move_source/move_source.py and | 
|  | should contain no behavioral changes. | 
|  |  | 
|  | Bug: 36063 | 
|  | ''' | 
|  |  | 
|  | if change_id: | 
|  | commit_message += ''' | 
|  | Change-Id: %s | 
|  | ''' % change_id | 
|  | return commit_message | 
|  |  | 
|  |  | 
|  | """ | 
|  | This finds all header files in a source directory that are referenced by | 
|  | files outside the source directory. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def find_externally_referenced_headers(source): | 
|  | externally_referenced_headers = set() | 
|  | jiri_grep_args = ['jiri', 'grep', '#include .%s' % source] | 
|  | logging.debug('Running %s' % jiri_grep_args) | 
|  |  | 
|  | # search for all includes | 
|  | grep_results = subprocess.check_output(jiri_grep_args, cwd=fuchsia_root) | 
|  | for line in grep_results.splitlines(): | 
|  | print line | 
|  | file, include = line.split(':', 1) | 
|  | # filter out includes from within the source directory | 
|  | if not os.path.normpath(file).startswith(source): | 
|  | include = include[len('#include "'):-1] | 
|  | externally_referenced_headers.add(include) | 
|  |  | 
|  | logging.debug('Externally referenced headers: %s' % | 
|  | externally_referenced_headers) | 
|  | return externally_referenced_headers | 
|  |  | 
|  |  | 
|  | """ | 
|  | This runs 'gn desc' and parses the output to produce a representation of the | 
|  | build graph. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def extract_build_graph(label_or_pattern='//*'): | 
|  | out_dir = subprocess.check_output(['fx', 'get-build-dir']).strip() | 
|  |  | 
|  | args = ['fx', 'gn', 'desc', out_dir, label_or_pattern, '--format=json', | 
|  | '--all-toolchains'] | 
|  | json_build_graph = subprocess.check_output(args) | 
|  | return json.loads(json_build_graph) | 
|  |  | 
|  |  | 
|  | """ | 
|  | Moves everything from the directory 'source' to 'dest' | 
|  | """ | 
|  |  | 
|  |  | 
|  | def move_directory(source, dest, dry_run, repository=fuchsia_root): | 
|  | # git mv requires the parent of the destination directory to exist in | 
|  | # order to move a directory. | 
|  | dest_parent = os.path.dirname(dest) | 
|  | dest_parent_abs = os.path.join(repository, dest_parent) | 
|  | if not os.path.exists(dest_parent_abs): | 
|  | os.makedirs(dest_parent_abs) | 
|  | run_command(['git', 'mv', source, dest], dry_run, cwd=repository) | 
|  |  | 
|  | """ | 
|  | Updates all references to 'source' in files in the directory 'dest'. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def update_all_references(source, dest, dry_run): | 
|  | for dirpath, dirnames, filenames in os.walk(dest): | 
|  | for name in filenames: | 
|  | filepath = os.path.join(dirpath, name) | 
|  | logging.debug('converting %s to %s in %s' % (source, dest, filepath)) | 
|  |  | 
|  | # On a dry run, verify that the input_file can be read | 
|  | # On a normal run, read the input, write with new references | 
|  | # to a temp file, and then swap the temp file over the input file | 
|  | with open(filepath, 'r') as input_file: | 
|  | lines = input_file.readlines() | 
|  | if not dry_run: | 
|  | filepath_temp = filepath + ".temp" | 
|  | with open(filepath_temp, 'w') as output_file: | 
|  | for line in lines: | 
|  | output_file.write(re.sub(source, dest, line)) | 
|  | os.rename(filepath_temp, filepath) | 
|  |  | 
|  |  | 
|  | """ | 
|  | Make a git commit with the provided commit message. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def commit_with_message(commit_message, dry_run, no_commit): | 
|  | with tempfile.NamedTemporaryFile(delete=False) as commit_message_file: | 
|  | commit_message_file.write(commit_message) | 
|  | if not no_commit: | 
|  | run_command(['git', 'commit', '-a', '--file=%s' % | 
|  | commit_message_file.name], dry_run) | 
|  | os.remove(commit_message_file.name) | 
|  |  | 
|  |  | 
|  | """ | 
|  | Logs a command and then runs it in the Fuchsia root if dry_run is false. | 
|  | """ | 
|  |  | 
|  |  | 
|  | def run_command(command, dry_run, cwd=fuchsia_root): | 
|  | logging.debug('Running %s in cwd %s' % (command, cwd)) | 
|  | if not dry_run: | 
|  | subprocess.check_call(command, cwd=cwd) | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | parser = argparse.ArgumentParser( | 
|  | description=tool_description, | 
|  | formatter_class=argparse.RawDescriptionHelpFormatter) | 
|  |  | 
|  | parser.add_argument('source', help='Source path') | 
|  | parser.add_argument('dest', help='Destination path') | 
|  | parser.add_argument('--no-branch', action='store_true', | 
|  | help='Do not create a git branch for this move') | 
|  | parser.add_argument('--no-commit', action='store_true', | 
|  | help='Do not create a git commit for this move') | 
|  | parser.add_argument('--change-id', | 
|  | help='Change-Id value to use in generated commit. ' + | 
|  | 'Use this to generate new commits associated with ' + | 
|  | 'an existing review') | 
|  | parser.add_argument('--dry-run', '-n', action='store_true', | 
|  | help='Dry run - log commands, but do not modify tree') | 
|  | parser.add_argument('--verbose', '-v', action='store_true', | 
|  | help='Enable verbose debug logging') | 
|  |  | 
|  | args = parser.parse_args() | 
|  |  | 
|  | if args.verbose: | 
|  | logging.getLogger().setLevel(logging.DEBUG) | 
|  |  | 
|  | source = os.path.normpath(args.source) | 
|  | source_abs = os.path.join(fuchsia_root, source) | 
|  | dest = os.path.normpath(args.dest) | 
|  | dest_abs = os.path.join(fuchsia_root, args.dest) | 
|  |  | 
|  | if not os.path.exists(source): | 
|  | print('Source path %s does not exist within source tree' % source) | 
|  | return 1 | 
|  |  | 
|  | if os.path.exists(dest): | 
|  | print('Destination path %s already exists' % dest) | 
|  | return 1 | 
|  |  | 
|  | # create fresh branch | 
|  | if not args.no_branch: | 
|  | create_branch_for_move(source, dest, args.dry_run) | 
|  |  | 
|  | # query build graph | 
|  | build_graph = extract_build_graph() | 
|  |  | 
|  | # find forwarding targets to create | 
|  | forwarding_targets = find_referenced_targets(build_graph, source) | 
|  |  | 
|  | # Set of forwarding headers to generate, identified by path | 
|  | forwarding_headers = find_externally_referenced_headers(source) | 
|  |  | 
|  | # Libraries in garnet/public/lib use //garnet/public:config which adds | 
|  | # //garnet/public to the include path, so includes will look like | 
|  | # #include <lib/foo/...>. Look for includes of that type as well, and | 
|  | # generate forwarding headers as if these were fully qualified includes | 
|  | # if that header exists in garnet/public/lib | 
|  | garnet_public_prefix = 'garnet/public/' | 
|  | if source.startswith(garnet_public_prefix): | 
|  | relative_source = source[len(garnet_public_prefix):] | 
|  | relative_headers = find_externally_referenced_headers(relative_source) | 
|  | for relative_header in relative_headers: | 
|  | relative_path = garnet_public_prefix + relative_header | 
|  | if os.path.exists(os.path.join(fuchsia_root, relative_path)): | 
|  | forwarding_headers.add(garnet_public_prefix + relative_header) | 
|  |  | 
|  | # search for other references to old path | 
|  | other_references = find_unknown_references(source) | 
|  |  | 
|  | # git mv to destination | 
|  | move_directory(source, dest, args.dry_run) | 
|  |  | 
|  | # generate forwarding targets | 
|  | write_forwarding_build_rules( | 
|  | forwarding_targets, source, dest, args.dry_run) | 
|  |  | 
|  | # generate forwarding headers | 
|  | for header in forwarding_headers: | 
|  | generate_forwarding_header(header, source, dest, args.dry_run) | 
|  |  | 
|  | # update references in destination directory | 
|  | update_all_references(source, dest, args.dry_run) | 
|  |  | 
|  | # format code | 
|  | run_command(['fx', 'format-code'], args.dry_run) | 
|  |  | 
|  | # generate commit message | 
|  | commit_message = generate_commit_message(source, dest, | 
|  | forwarding_targets, | 
|  | forwarding_headers, | 
|  | args.change_id) | 
|  |  | 
|  | logging.debug(commit_message) | 
|  |  | 
|  | # commit with message | 
|  | commit_with_message(commit_message, args.dry_run, args.no_commit) | 
|  |  | 
|  | # log other references | 
|  | if len(other_references): | 
|  | print('%s references to old location found, please update manually' % | 
|  | len(other_references)) | 
|  | for line in other_references: | 
|  | print('  %s' % line) | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | sys.exit(main()) |