blob: 24419a71035fe1b538fa2e666f1f5346b69fb03d [file] [log] [blame]
Luca Farsi5717d6f2023-12-28 15:09:28 -08001# Copyright 2024, The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Script to build only the necessary modules for general-tests along
16
17with whatever other targets are passed in.
18"""
19
20import argparse
21from collections.abc import Sequence
22import json
23import os
24import pathlib
25import re
26import subprocess
27import sys
Luca Farsi88feffc2024-02-13 12:09:08 -080028from typing import Any
Luca Farsi5717d6f2023-12-28 15:09:28 -080029
30import test_mapping_module_retriever
31
32
33# List of modules that are always required to be in general-tests.zip
34REQUIRED_MODULES = frozenset(
35 ['cts-tradefed', 'vts-tradefed', 'compatibility-host-util', 'soong_zip']
36)
37
38
39def build_test_suites(argv):
40 args = parse_args(argv)
41
Luca Farsic18da792024-01-22 14:56:13 -080042 if not os.environ.get('BUILD_NUMBER')[0] == 'P':
Luca Farsi5717d6f2023-12-28 15:09:28 -080043 build_everything(args)
44 return
45
46 # Call the class to map changed files to modules to build.
47 # TODO(lucafarsi): Move this into a replaceable class.
48 build_affected_modules(args)
49
50
51def parse_args(argv):
52 argparser = argparse.ArgumentParser()
53 argparser.add_argument(
54 'extra_targets', nargs='*', help='Extra test suites to build.'
55 )
56 argparser.add_argument('--target_product')
57 argparser.add_argument('--target_release')
58 argparser.add_argument(
59 '--with_dexpreopt_boot_img_and_system_server_only', action='store_true'
60 )
61 argparser.add_argument('--dist_dir')
62 argparser.add_argument('--change_info', nargs='?')
63 argparser.add_argument('--extra_required_modules', nargs='*')
64
65 return argparser.parse_args()
66
67
68def build_everything(args: argparse.Namespace):
69 build_command = base_build_command(args)
70 build_command.append('general-tests')
71
Luca Farsib559eef2024-01-17 16:14:55 -080072 run_command(build_command, print_output=True)
Luca Farsi5717d6f2023-12-28 15:09:28 -080073
74
75def build_affected_modules(args: argparse.Namespace):
76 modules_to_build = find_modules_to_build(
77 pathlib.Path(args.change_info), args.extra_required_modules
78 )
79
80 # Call the build command with everything.
81 build_command = base_build_command(args)
82 build_command.extend(modules_to_build)
83
Luca Farsib559eef2024-01-17 16:14:55 -080084 run_command(build_command, print_output=True)
Luca Farsi5717d6f2023-12-28 15:09:28 -080085
Luca Farsib559eef2024-01-17 16:14:55 -080086 zip_build_outputs(modules_to_build, args.dist_dir, args.target_release)
Luca Farsi5717d6f2023-12-28 15:09:28 -080087
88
89def base_build_command(args: argparse.Namespace) -> list:
90 build_command = []
91 build_command.append('time')
92 build_command.append('./build/soong/soong_ui.bash')
93 build_command.append('--make-mode')
94 build_command.append('dist')
95 build_command.append('DIST_DIR=' + args.dist_dir)
96 build_command.append('TARGET_PRODUCT=' + args.target_product)
97 build_command.append('TARGET_RELEASE=' + args.target_release)
Luca Farsi212d3862024-01-12 11:22:06 -080098 if args.with_dexpreopt_boot_img_and_system_server_only:
99 build_command.append('WITH_DEXPREOPT_BOOT_IMG_AND_SYSTEM_SERVER_ONLY=true')
Luca Farsi5717d6f2023-12-28 15:09:28 -0800100 build_command.extend(args.extra_targets)
101
102 return build_command
103
104
Luca Farsib559eef2024-01-17 16:14:55 -0800105def run_command(
106 args: list[str],
Luca Farsi88feffc2024-02-13 12:09:08 -0800107 env: dict[str, str] = os.environ,
Luca Farsib559eef2024-01-17 16:14:55 -0800108 print_output: bool = False,
109) -> str:
Luca Farsi5717d6f2023-12-28 15:09:28 -0800110 result = subprocess.run(
111 args=args,
112 text=True,
113 capture_output=True,
114 check=False,
Luca Farsib559eef2024-01-17 16:14:55 -0800115 env=env,
Luca Farsi5717d6f2023-12-28 15:09:28 -0800116 )
117 # If the process failed, print its stdout and propagate the exception.
118 if not result.returncode == 0:
119 print('Build command failed! output:')
120 print('stdout: ' + result.stdout)
121 print('stderr: ' + result.stderr)
122
123 result.check_returncode()
Luca Farsib559eef2024-01-17 16:14:55 -0800124
125 if print_output:
126 print(result.stdout)
127
Luca Farsi5717d6f2023-12-28 15:09:28 -0800128 return result.stdout
129
130
131def find_modules_to_build(
Luca Farsi88feffc2024-02-13 12:09:08 -0800132 change_info: pathlib.Path, extra_required_modules: list[str]
133) -> set[str]:
Luca Farsi5717d6f2023-12-28 15:09:28 -0800134 changed_files = find_changed_files(change_info)
135
136 test_mappings = test_mapping_module_retriever.GetTestMappings(
137 changed_files, set()
138 )
139
140 # Soong_zip is required to generate the output zip so always build it.
141 modules_to_build = set(REQUIRED_MODULES)
142 if extra_required_modules:
143 modules_to_build.update(extra_required_modules)
144
145 modules_to_build.update(find_affected_modules(test_mappings, changed_files))
146
147 return modules_to_build
148
149
Luca Farsi88feffc2024-02-13 12:09:08 -0800150def find_changed_files(change_info: pathlib.Path) -> set[str]:
Luca Farsi5717d6f2023-12-28 15:09:28 -0800151 with open(change_info) as change_info_file:
152 change_info_contents = json.load(change_info_file)
153
154 changed_files = set()
155
156 for change in change_info_contents['changes']:
157 project_path = change.get('projectPath') + '/'
158
159 for revision in change.get('revisions'):
160 for file_info in revision.get('fileInfos'):
161 changed_files.add(project_path + file_info.get('path'))
162
163 return changed_files
164
165
166def find_affected_modules(
Luca Farsi88feffc2024-02-13 12:09:08 -0800167 test_mappings: dict[str, Any], changed_files: set[str]
168) -> set[str]:
Luca Farsi5717d6f2023-12-28 15:09:28 -0800169 modules = set()
170
171 # The test_mappings object returned by GetTestMappings is organized as
172 # follows:
173 # {
174 # 'test_mapping_file_path': {
175 # 'group_name' : [
176 # 'name': 'module_name',
177 # ],
178 # }
179 # }
180 for test_mapping in test_mappings.values():
181 for group in test_mapping.values():
182 for entry in group:
183 module_name = entry.get('name', None)
184
185 if not module_name:
186 continue
187
188 file_patterns = entry.get('file_patterns')
189 if not file_patterns:
190 modules.add(module_name)
191 continue
192
193 if matches_file_patterns(file_patterns, changed_files):
194 modules.add(module_name)
195 continue
196
197 return modules
198
199
200# TODO(lucafarsi): Share this logic with the original logic in
201# test_mapping_test_retriever.py
202def matches_file_patterns(
Luca Farsi88feffc2024-02-13 12:09:08 -0800203 file_patterns: list[set], changed_files: set[str]
Luca Farsi5717d6f2023-12-28 15:09:28 -0800204) -> bool:
205 for changed_file in changed_files:
206 for pattern in file_patterns:
207 if re.search(pattern, changed_file):
208 return True
209
210 return False
211
212
Luca Farsib559eef2024-01-17 16:14:55 -0800213def zip_build_outputs(
Luca Farsi88feffc2024-02-13 12:09:08 -0800214 modules_to_build: set[str], dist_dir: str, target_release: str
Luca Farsib559eef2024-01-17 16:14:55 -0800215):
Luca Farsi5717d6f2023-12-28 15:09:28 -0800216 src_top = os.environ.get('TOP', os.getcwd())
217
218 # Call dumpvars to get the necessary things.
219 # TODO(lucafarsi): Don't call soong_ui 4 times for this, --dumpvars-mode can
220 # do it but it requires parsing.
Luca Farsi88feffc2024-02-13 12:09:08 -0800221 host_out_testcases = pathlib.Path(
222 get_soong_var('HOST_OUT_TESTCASES', target_release)
223 )
224 target_out_testcases = pathlib.Path(
225 get_soong_var('TARGET_OUT_TESTCASES', target_release)
226 )
227 product_out = pathlib.Path(get_soong_var('PRODUCT_OUT', target_release))
228 soong_host_out = pathlib.Path(get_soong_var('SOONG_HOST_OUT', target_release))
229 host_out = pathlib.Path(get_soong_var('HOST_OUT', target_release))
Luca Farsi5717d6f2023-12-28 15:09:28 -0800230
231 # Call the class to package the outputs.
232 # TODO(lucafarsi): Move this code into a replaceable class.
233 host_paths = []
234 target_paths = []
Luca Farsi88feffc2024-02-13 12:09:08 -0800235 host_config_files = []
236 target_config_files = []
Luca Farsi5717d6f2023-12-28 15:09:28 -0800237 for module in modules_to_build:
238 host_path = os.path.join(host_out_testcases, module)
239 if os.path.exists(host_path):
240 host_paths.append(host_path)
Luca Farsi88feffc2024-02-13 12:09:08 -0800241 collect_config_files(src_top, host_path, host_config_files)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800242
243 target_path = os.path.join(target_out_testcases, module)
244 if os.path.exists(target_path):
245 target_paths.append(target_path)
Luca Farsi88feffc2024-02-13 12:09:08 -0800246 collect_config_files(src_top, target_path, target_config_files)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800247
Luca Farsi88feffc2024-02-13 12:09:08 -0800248 zip_test_configs_zips(
249 dist_dir, host_out, product_out, host_config_files, target_config_files
250 )
251
252 zip_command = base_zip_command(host_out, dist_dir, 'general-tests.zip')
Luca Farsi5717d6f2023-12-28 15:09:28 -0800253
254 # Add host testcases.
255 zip_command.append('-C')
256 zip_command.append(os.path.join(src_top, soong_host_out))
257 zip_command.append('-P')
258 zip_command.append('host/')
259 for path in host_paths:
260 zip_command.append('-D')
261 zip_command.append(path)
262
263 # Add target testcases.
264 zip_command.append('-C')
265 zip_command.append(os.path.join(src_top, product_out))
266 zip_command.append('-P')
267 zip_command.append('target')
268 for path in target_paths:
269 zip_command.append('-D')
270 zip_command.append(path)
271
272 # TODO(lucafarsi): Push this logic into a general-tests-minimal build command
273 # Add necessary tools. These are also hardcoded in general-tests.mk.
274 framework_path = os.path.join(soong_host_out, 'framework')
275
276 zip_command.append('-C')
277 zip_command.append(framework_path)
278 zip_command.append('-P')
279 zip_command.append('host/tools')
280 zip_command.append('-f')
281 zip_command.append(os.path.join(framework_path, 'cts-tradefed.jar'))
282 zip_command.append('-f')
283 zip_command.append(
284 os.path.join(framework_path, 'compatibility-host-util.jar')
285 )
286 zip_command.append('-f')
287 zip_command.append(os.path.join(framework_path, 'vts-tradefed.jar'))
288
Luca Farsib559eef2024-01-17 16:14:55 -0800289 run_command(zip_command, print_output=True)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800290
291
Luca Farsi88feffc2024-02-13 12:09:08 -0800292def collect_config_files(
293 src_top: pathlib.Path, root_dir: pathlib.Path, config_files: list[str]
294):
295 for root, dirs, files in os.walk(os.path.join(src_top, root_dir)):
296 for file in files:
297 if file.endswith('.config'):
298 config_files.append(os.path.join(root_dir, file))
299
300
301def base_zip_command(
302 host_out: pathlib.Path, dist_dir: pathlib.Path, name: str
303) -> list[str]:
304 return [
305 'time',
306 os.path.join(host_out, 'bin', 'soong_zip'),
307 '-d',
308 '-o',
309 os.path.join(dist_dir, name),
310 ]
311
312
313# generate general-tests_configs.zip which contains all of the .config files
314# that were built and general-tests_list.zip which contains a text file which
315# lists all of the .config files that are in general-tests_configs.zip.
316#
317# general-tests_comfigs.zip is organized as follows:
318# /
319# host/
320# testcases/
321# test_1.config
322# test_2.config
323# ...
324# target/
325# testcases/
326# test_1.config
327# test_2.config
328# ...
329#
330# So the process is we write out the paths to all the host config files into one
331# file and all the paths to the target config files in another. We also write
332# the paths to all the config files into a third file to use for
333# general-tests_list.zip.
334def zip_test_configs_zips(
335 dist_dir: pathlib.Path,
336 host_out: pathlib.Path,
337 product_out: pathlib.Path,
338 host_config_files: list[str],
339 target_config_files: list[str],
340):
341 with open(
342 os.path.join(host_out, 'host_general-tests_list'), 'w'
343 ) as host_list_file, open(
344 os.path.join(product_out, 'target_general-tests_list'), 'w'
345 ) as target_list_file, open(
346 os.path.join(host_out, 'general-tests_list'), 'w'
347 ) as list_file:
348
349 for config_file in host_config_files:
350 host_list_file.write(config_file + '\n')
351 list_file.write('host/' + os.path.relpath(config_file, host_out) + '\n')
352
353 for config_file in target_config_files:
354 target_list_file.write(config_file + '\n')
355 list_file.write(
356 'target/' + os.path.relpath(config_file, product_out) + '\n'
357 )
358
359 tests_config_zip_command = base_zip_command(
360 host_out, dist_dir, 'general-tests_configs.zip'
361 )
362 tests_config_zip_command.append('-P')
363 tests_config_zip_command.append('host')
364 tests_config_zip_command.append('-C')
365 tests_config_zip_command.append(host_out)
366 tests_config_zip_command.append('-l')
367 tests_config_zip_command.append(
368 os.path.join(host_out, 'host_general-tests_list')
369 )
370 tests_config_zip_command.append('-P')
371 tests_config_zip_command.append('target')
372 tests_config_zip_command.append('-C')
373 tests_config_zip_command.append(product_out)
374 tests_config_zip_command.append('-l')
375 tests_config_zip_command.append(
376 os.path.join(product_out, 'target_general-tests_list')
377 )
378 run_command(tests_config_zip_command, print_output=True)
379
380 tests_list_zip_command = base_zip_command(
381 host_out, dist_dir, 'general-tests_list.zip'
382 )
383 tests_list_zip_command.append('-C')
384 tests_list_zip_command.append(host_out)
385 tests_list_zip_command.append('-f')
386 tests_list_zip_command.append(os.path.join(host_out, 'general-tests_list'))
387 run_command(tests_list_zip_command, print_output=True)
388
389
Luca Farsib559eef2024-01-17 16:14:55 -0800390def get_soong_var(var: str, target_release: str) -> str:
391 new_env = os.environ.copy()
392 new_env['TARGET_RELEASE'] = target_release
393
Luca Farsi5717d6f2023-12-28 15:09:28 -0800394 value = run_command(
Luca Farsib559eef2024-01-17 16:14:55 -0800395 ['./build/soong/soong_ui.bash', '--dumpvar-mode', '--abs', var],
396 env=new_env,
Luca Farsi5717d6f2023-12-28 15:09:28 -0800397 ).strip()
398 if not value:
399 raise RuntimeError('Necessary soong variable ' + var + ' not found.')
400
401 return value
402
403
404def main(argv):
405 build_test_suites(sys.argv)