import argparse
import datetime
import difflib
import glob
import os.path
import re
import subprocess
import sys
TERMINAL_ERROR_COLOR = '\033[91m'
TERMINAL_RESET_COLOR = '\033[0m'
_HEADER = """# Copyright %d The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# NOTE: this file is generated by build/ios/update_bundle_filelist.py
# If it requires updating, you should get a presubmit error with
# instructions on how to regenerate. Otherwise, do not edit.
""" % (datetime.datetime.now().year)
_HEADER_PATTERN = re.compile(r"""# Copyright [0-9]+ The Chromium Authors
""")
_HEADER_HEIGHT = 6
_START_IGNORE_EXPANSIONS_OUTSIDE_GLOBLIST_DIR = '# push(ignore-relative)'
_STOP_IGNORE_EXPANSIONS_OUTSIDE_GLOBLIST_DIR = '# pop(ignore-relative)'
def parse_filelist(filelist_name):
try:
with open(filelist_name) as filelist:
unfiltered = [l for l in filelist]
header = ''.join(unfiltered[:_HEADER_HEIGHT])
files = sorted(l.strip() for l in unfiltered[_HEADER_HEIGHT:])
return (files, header)
except Exception as e:
print_error(f'Could not read file list: {filelist_name}', f'{type(e)}: {e}')
return []
def get_git_command_name():
if sys.platform.startswith('win'):
return 'git.bat'
return 'git'
def get_tracked_files(directory, globroot, repository_root_relative, verbose):
try:
if os.getcwd().startswith('/google/cog/cloud'):
files = []
for root, _, filenames in os.walk(directory):
files.extend([os.path.join(root, f) for f in filenames])
return set(files)
cmd = [get_git_command_name(), 'ls-files', '--error-unmatch', directory]
with subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=globroot) as p:
output = p.communicate()
if p.returncode != 0:
if verbose:
print_error(
f'Could not gather a list of tracked files in {directory}',
f'{output[1]}')
return set()
files = [f.decode('utf-8') for f in output[0].splitlines()]
if repository_root_relative:
files = ['//' + f for f in files]
files = [f.replace('\\', '/') for f in files]
return set(files)
except Exception as e:
if verbose:
print_error(f'Could not gather a list of tracked files in {directory}',
f'{type(e)}: {e}')
return set()
def combine_potentially_repository_root_relative_paths(a, b):
if b.startswith('//'):
return (a + b, True)
else:
return (os.path.join(a, b), False)
def parse_and_expand_globlist(globlist_name, glob_root):
if glob_root.endswith('/'):
glob_root = glob_root[:-1]
check_expansions_outside_globlist_dir = True
globlist_dir = os.path.dirname(globlist_name)
with open(globlist_name) as globlist:
files = set()
to_check = set()
for g in globlist:
g = g.strip()
if not g:
continue
if g == _START_IGNORE_EXPANSIONS_OUTSIDE_GLOBLIST_DIR:
check_expansions_outside_globlist_dir = False
elif g == _STOP_IGNORE_EXPANSIONS_OUTSIDE_GLOBLIST_DIR:
check_expansions_outside_globlist_dir = True
if not g or g.startswith('#'):
continue
is_exclusion = g.startswith('-')
if is_exclusion:
g = g[1:]
(combined,
root_relative) = combine_potentially_repository_root_relative_paths(
glob_root, g)
prefix_size = len(glob_root)
if not root_relative:
prefix_size += 1
expansion = glob.glob(combined, recursive=True)
expansion = [f for f in expansion if os.path.isfile(f)]
if check_expansions_outside_globlist_dir:
for f in expansion:
relative = os.path.relpath(f, globlist_dir)
if relative.startswith('..'):
raise Exception(f'Globlist expansion outside globlist dir: {f}')
expansion = [f[prefix_size:] for f in expansion]
expansion = [f.replace('\\', '/') for f in expansion]
if is_exclusion:
files = files.difference(expansion)
else:
files = files.union(expansion)
return sorted(files)
def compare_lists(a, b):
differ = difflib.Differ()
full_diff = differ.compare(a, b)
lines = [d for d in full_diff if not d.startswith(' ')]
additions = [l[2:] for l in lines if l.startswith('+ ')]
removals = [l[2:] for l in lines if l.startswith('- ')]
return (additions, removals)
def write_filelist(filelist_name, files, header):
try:
with open(filelist_name, 'w', encoding='utf-8', newline='') as filelist:
if not _HEADER_PATTERN.search(header):
header = _HEADER
filelist.write(header)
for file in files:
filelist.write(f'{file}\n')
except Exception as e:
print_error(f'Could not write file list: {filelist_name}',
f'{type(e)}: {e}')
return []
def process_filelist(filelist, globlist, globroot, check=False, verbose=False):
files_from_globlist = []
try:
files_from_globlist = parse_and_expand_globlist(globlist, globroot)
except Exception as e:
if verbose:
print_error(f'Could not read glob list: {globlist}', f'{type(e)}: {e}')
return 1
(files, header) = parse_filelist(filelist)
(additions, removals) = compare_lists(files, files_from_globlist)
to_ignore = set()
if additions:
directories = set([os.path.dirname(f) for f in additions])
tracked_files = set()
for d in directories:
(combined,
root_relative) = combine_potentially_repository_root_relative_paths(
globroot, d)
relative = os.path.relpath(combined, globroot)
tracked_files = tracked_files.union(
get_tracked_files(relative, globroot, root_relative, verbose))
to_ignore = set(additions).difference(tracked_files)
additions = [f for f in additions if f in tracked_files]
files_from_globlist = [f for f in files_from_globlist if f not in to_ignore]
if check:
if not _HEADER_PATTERN.search(header):
if verbose:
print_error(f'Unexpected header for {filelist}', f'{header}')
return 1
if not additions and not removals:
return 0
if verbose:
pretty_additions = ['+ ' + f for f in additions]
pretty_removals = ['- ' + f for f in removals]
pretty_diff = '\n'.join(pretty_additions + pretty_removals)
print_error('File list does not match glob expansion', f'{pretty_diff}')
return 1
else:
write_filelist(filelist, files_from_globlist, header)
return 0
def main(args):
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('filelist', help='Contains one file per line')
parser.add_argument('globlist',
help='Contains globs that, when expanded, '
'should match the filelist. Use '
'--help for details on syntax')
parser.add_argument('globroot',
help='Directory from which globs are relative')
parser.add_argument('-c',
'--check',
action='store_true',
help='Prevents modifying the file list')
parser.add_argument('-v',
'--verbose',
action='store_true',
help='Use this to print details on differences')
args = parser.parse_args()
return process_filelist(args.filelist,
args.globlist,
args.globroot,
check=args.check,
verbose=args.verbose)
def print_error(error_message, error_info):
color_start, color_end = adapted_color_for_output(TERMINAL_ERROR_COLOR,
TERMINAL_RESET_COLOR)
error_message = color_start + 'ERROR: ' + error_message + color_end
if len(error_info) > 0:
error_message = error_message + '\n' + error_info
print(error_message, file=sys.stderr)
def adapted_color_for_output(color_start, color_end):
if not sys.stdout.isatty():
return '', ''
return color_start, color_end
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))