blob: 70a7966ee52b6d6a437b7378ad3deee3f6a587e1 [file] [log] [blame]
Ian Vollick00424e52023-02-03 10:35:501#!/usr/bin/env python3
2# Copyright 2023 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""
6Updates .filelist files using data from corresponding .globlist files (or
7checks whether they are up to date).
8
9bundle_data targets require an explicit source list, but maintaining these large
10lists can be cumbersome. This script aims to simplify the process of updating
11these lists by either expanding globs to update file lists or check that an
12existing file list matches such an expansion (i.e., checking during presubmit).
13
14The .globlist file contains a list of globs that will be expanded to either
15compare or replace a corresponding .filelist. It is possible to exclude items
16from the file list with globs as well. These lines are prefixed with '-' and are
17processed in order, so be sure that exclusions succeed inclusions in the list of
18globs. Comments and empty lines are permitted in .globfiles; comments are
19prefixed with '#'.
20
21By convention, the base name of the .globlist and .filelist files matches the
22label of their corresponding bundle_data from the .gn file. In order to ensure
23that these filelists don't get stale, there should also be a PRESUBMIT.py
24which uses this script to check that list is up to date.
25
26By default, the script will update the file list to match the expanded globs.
27"""
28
29import argparse
30import datetime
31import difflib
32import glob
33import os.path
34import re
35import sys
36
37# Character to set colors in terminal. Taken, along with the printing routine
38# below, from update_deps.py.
39TERMINAL_ERROR_COLOR = '\033[91m'
40TERMINAL_RESET_COLOR = '\033[0m'
41
42_HEADER = """# Copyright %d The Chromium Authors
43# Use of this source code is governed by a BSD-style license that can be
44# found in the LICENSE file.
45# NOTE: this file is generated by build/ios/update_bundle_filelist.py
46# If it requires updating, you should get a presubmit error with
47# instructions on how to regenerate. Otherwise, do not edit.
48""" % (datetime.datetime.now().year)
49
50_HEADER_PATTERN = re.compile(r"""# Copyright [0-9]+ The Chromium Authors
51# Use of this source code is governed by a BSD-style license that can be
52# found in the LICENSE file.
53# NOTE: this file is generated by build/ios/update_bundle_filelist.py
54# If it requires updating, you should get a presubmit error with
55# instructions on how to regenerate. Otherwise, do not edit.
56""")
57
58_HEADER_HEIGHT = 6
59
60
61def parse_filelist(filelist_name):
62 try:
63 with open(filelist_name) as filelist:
64 unfiltered = [l for l in filelist]
65 header = ''.join(unfiltered[:_HEADER_HEIGHT])
66 files = sorted(l.strip() for l in unfiltered[_HEADER_HEIGHT:])
67 return (files, header)
68 except Exception as e:
69 print_error(f'Could not read file list: {filelist_name}', f'{type(e)}: {e}')
70 return []
71
72
73def parse_and_expand_globlist(globlist_name, glob_root):
74 try:
75 # The following expects glob_root not to end in a trailing slash.
76 if glob_root.endswith('/'):
77 glob_root = glob_root[:-1]
78
79 with open(globlist_name) as globlist:
80 files = []
81 for g in globlist:
82 g = g.strip()
83
84 # Ignore blank lines and comments.
85 if not g or g.startswith('#'):
86 continue
87
88 # Exclusions are prefixed with '-'.
89 is_exclusion = g.startswith('-')
90 if is_exclusion:
91 g = g[1:]
92
93 prefix_size = len(glob_root)
94 full_glob = ''
95
96 if g.startswith('//'):
97 # If globlist is relative to the repository root, os.path
98 # will consider it absolute and os.path.join will fail.
99 # In this case, we can simply concatenate the paths.
100 full_glob = glob_root + g
101 else:
102 full_glob = os.path.join(glob_root, g)
103 # We need to account for the separator.
104 prefix_size += 1
105
106 expansion = glob.glob(full_glob, recursive=True)
107
108 # Filter out directories.
109 expansion = [f for f in expansion if os.path.isfile(f)]
110
111 # Make relative to |glob_root|.
112 expansion = [f[prefix_size:] for f in expansion]
113
114 if is_exclusion:
Bruce Dawson29955b02023-02-04 00:03:48115 files = [f for f in files if f.replace('\\', '/') not in expansion]
Ian Vollick00424e52023-02-03 10:35:50116 else:
117 files += expansion
118
Bruce Dawson29955b02023-02-04 00:03:48119 # Handle Windows backslashes
120 files = [f.replace('\\', '/') for f in files]
Ian Vollick00424e52023-02-03 10:35:50121 files.sort()
122 return files
123
124 except Exception as e:
125 print_error(f'Could not read glob list: {globlist_name}', f'{type(e)}: {e}')
126 return []
127
128
129def compare_lists(a, b, verbose):
130 differ = difflib.Differ()
131 full_diff = differ.compare(a, b)
132 diff = '\n'.join([d for d in full_diff if not d.startswith(' ')])
133 if diff:
134 if verbose:
135 print_error('File list does not match glob expansion', f'{diff}')
136 return False
137 return True
138
139
140def write_filelist(filelist_name, files, header):
141 try:
Bruce Dawson29955b02023-02-04 00:03:48142 with open(filelist_name, 'w', encoding='utf-8', newline='') as filelist:
Ian Vollick00424e52023-02-03 10:35:50143 if not _HEADER_PATTERN.search(header):
144 header = _HEADER
145 filelist.write(header)
146 for file in files:
147 filelist.write(f'{file}\n')
148 except Exception as e:
149 print_error(f'Could not write file list: {filelist_name}',
150 f'{type(e)}: {e}')
151 return []
152
153
154def process_filelist(filelist, globlist, globroot, check=False, verbose=False):
155 files_from_globlist = parse_and_expand_globlist(globlist, globroot)
156 (files, header) = parse_filelist(filelist)
157 if check:
158 if not _HEADER_PATTERN.search(header):
159 if verbose:
160 print_error(f'Unexpected header for {filelist}', f'{header}')
161 return 1
162 if compare_lists(files, files_from_globlist, verbose):
163 return 0
164 return 1
165 else:
166 write_filelist(filelist, files_from_globlist, header)
167 return 0
168
169
170def main(args):
171 parser = argparse.ArgumentParser(
172 description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
173 parser.add_argument('filelist', help='Contains one file per line')
174 parser.add_argument('globlist',
175 help='Contains globs that, when expanded, '
176 'should match the filelist. Use '
177 '--help for details on syntax')
178 parser.add_argument('globroot',
179 help='Directory from which globs are relative')
180 parser.add_argument('-c',
181 '--check',
182 action='store_true',
183 help='Prevents modifying the file list')
184 parser.add_argument('-v',
185 '--verbose',
186 action='store_true',
187 help='Use this to print details on differences')
188 args = parser.parse_args()
189 return process_filelist(args.filelist,
190 args.globlist,
191 args.globroot,
192 check=args.check,
193 verbose=args.verbose)
194
195
196def print_error(error_message, error_info):
197 """ Print the `error_message` with additional `error_info` """
198 color_start, color_end = adapted_color_for_output(TERMINAL_ERROR_COLOR,
199 TERMINAL_RESET_COLOR)
200
201 error_message = color_start + 'ERROR: ' + error_message + color_end
202 if len(error_info) > 0:
203 error_message = error_message + '\n' + error_info
204 print(error_message, file=sys.stderr)
205
206
207def adapted_color_for_output(color_start, color_end):
208 """ Returns a the `color_start`, `color_end` tuple if the output is a
209 terminal, or empty strings otherwise """
210 if not sys.stdout.isatty():
211 return '', ''
212 return color_start, color_end
213
214
215if __name__ == '__main__':
216 sys.exit(main(sys.argv[1:]))