blob: dad34f1b166a3ab2818968acd45ebe49bf08b8c6 [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
Luca Farsi11767d52024-03-07 13:33:57 -080039def build_test_suites(argv, extra_targets: set[str]):
Luca Farsi5717d6f2023-12-28 15:09:28 -080040 args = parse_args(argv)
41
Luca Farsi11767d52024-03-07 13:33:57 -080042 if is_optimization_enabled():
43 # Call the class to map changed files to modules to build.
44 # TODO(lucafarsi): Move this into a replaceable class.
45 build_affected_modules(args, extra_targets)
46 else:
47 build_everything(args, extra_targets)
Luca Farsi5717d6f2023-12-28 15:09:28 -080048
49
50def parse_args(argv):
51 argparser = argparse.ArgumentParser()
52 argparser.add_argument(
53 'extra_targets', nargs='*', help='Extra test suites to build.'
54 )
55 argparser.add_argument('--target_product')
56 argparser.add_argument('--target_release')
57 argparser.add_argument(
58 '--with_dexpreopt_boot_img_and_system_server_only', action='store_true'
59 )
60 argparser.add_argument('--dist_dir')
61 argparser.add_argument('--change_info', nargs='?')
Luca Farsi5717d6f2023-12-28 15:09:28 -080062
63 return argparser.parse_args()
64
65
Luca Farsi11767d52024-03-07 13:33:57 -080066def is_optimization_enabled() -> bool:
67 # TODO(lucafarsi): switch back to building only affected general-tests modules
68 # in presubmit once ready.
69 # if os.environ.get('BUILD_NUMBER')[0] == 'P':
70 # return True
71 return False
72
73
74def build_everything(args: argparse.Namespace, extra_targets: set[str]):
75 build_command = base_build_command(args, extra_targets)
Luca Farsi5717d6f2023-12-28 15:09:28 -080076 build_command.append('general-tests')
77
Luca Farsib559eef2024-01-17 16:14:55 -080078 run_command(build_command, print_output=True)
Luca Farsi5717d6f2023-12-28 15:09:28 -080079
80
Luca Farsi11767d52024-03-07 13:33:57 -080081def build_affected_modules(args: argparse.Namespace, extra_targets: set[str]):
Luca Farsi5717d6f2023-12-28 15:09:28 -080082 modules_to_build = find_modules_to_build(
83 pathlib.Path(args.change_info), args.extra_required_modules
84 )
85
86 # Call the build command with everything.
Luca Farsi11767d52024-03-07 13:33:57 -080087 build_command = base_build_command(args, extra_targets)
Luca Farsi5717d6f2023-12-28 15:09:28 -080088 build_command.extend(modules_to_build)
Luca Farsi5722c942024-02-21 12:09:10 -080089 # When not building general-tests we also have to build the general tests
90 # shared libs.
91 build_command.append('general-tests-shared-libs')
Luca Farsi5717d6f2023-12-28 15:09:28 -080092
Luca Farsib559eef2024-01-17 16:14:55 -080093 run_command(build_command, print_output=True)
Luca Farsi5717d6f2023-12-28 15:09:28 -080094
Luca Farsib559eef2024-01-17 16:14:55 -080095 zip_build_outputs(modules_to_build, args.dist_dir, args.target_release)
Luca Farsi5717d6f2023-12-28 15:09:28 -080096
97
Luca Farsi11767d52024-03-07 13:33:57 -080098def base_build_command(args: argparse.Namespace, extra_targets: set[str]) -> list:
Luca Farsi5717d6f2023-12-28 15:09:28 -080099 build_command = []
100 build_command.append('time')
101 build_command.append('./build/soong/soong_ui.bash')
102 build_command.append('--make-mode')
103 build_command.append('dist')
104 build_command.append('DIST_DIR=' + args.dist_dir)
105 build_command.append('TARGET_PRODUCT=' + args.target_product)
106 build_command.append('TARGET_RELEASE=' + args.target_release)
Luca Farsi212d3862024-01-12 11:22:06 -0800107 if args.with_dexpreopt_boot_img_and_system_server_only:
108 build_command.append('WITH_DEXPREOPT_BOOT_IMG_AND_SYSTEM_SERVER_ONLY=true')
Luca Farsi11767d52024-03-07 13:33:57 -0800109 build_command.extend(extra_targets)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800110
111 return build_command
112
113
Luca Farsib559eef2024-01-17 16:14:55 -0800114def run_command(
115 args: list[str],
Luca Farsi88feffc2024-02-13 12:09:08 -0800116 env: dict[str, str] = os.environ,
Luca Farsib559eef2024-01-17 16:14:55 -0800117 print_output: bool = False,
118) -> str:
Luca Farsi5717d6f2023-12-28 15:09:28 -0800119 result = subprocess.run(
120 args=args,
121 text=True,
122 capture_output=True,
123 check=False,
Luca Farsib559eef2024-01-17 16:14:55 -0800124 env=env,
Luca Farsi5717d6f2023-12-28 15:09:28 -0800125 )
126 # If the process failed, print its stdout and propagate the exception.
127 if not result.returncode == 0:
128 print('Build command failed! output:')
129 print('stdout: ' + result.stdout)
130 print('stderr: ' + result.stderr)
131
132 result.check_returncode()
Luca Farsib559eef2024-01-17 16:14:55 -0800133
134 if print_output:
135 print(result.stdout)
136
Luca Farsi5717d6f2023-12-28 15:09:28 -0800137 return result.stdout
138
139
140def find_modules_to_build(
Luca Farsi88feffc2024-02-13 12:09:08 -0800141 change_info: pathlib.Path, extra_required_modules: list[str]
142) -> set[str]:
Luca Farsi5717d6f2023-12-28 15:09:28 -0800143 changed_files = find_changed_files(change_info)
144
145 test_mappings = test_mapping_module_retriever.GetTestMappings(
146 changed_files, set()
147 )
148
149 # Soong_zip is required to generate the output zip so always build it.
150 modules_to_build = set(REQUIRED_MODULES)
151 if extra_required_modules:
152 modules_to_build.update(extra_required_modules)
153
154 modules_to_build.update(find_affected_modules(test_mappings, changed_files))
155
156 return modules_to_build
157
158
Luca Farsi88feffc2024-02-13 12:09:08 -0800159def find_changed_files(change_info: pathlib.Path) -> set[str]:
Luca Farsi5717d6f2023-12-28 15:09:28 -0800160 with open(change_info) as change_info_file:
161 change_info_contents = json.load(change_info_file)
162
163 changed_files = set()
164
165 for change in change_info_contents['changes']:
166 project_path = change.get('projectPath') + '/'
167
168 for revision in change.get('revisions'):
169 for file_info in revision.get('fileInfos'):
170 changed_files.add(project_path + file_info.get('path'))
171
172 return changed_files
173
174
175def find_affected_modules(
Luca Farsi88feffc2024-02-13 12:09:08 -0800176 test_mappings: dict[str, Any], changed_files: set[str]
177) -> set[str]:
Luca Farsi5717d6f2023-12-28 15:09:28 -0800178 modules = set()
179
180 # The test_mappings object returned by GetTestMappings is organized as
181 # follows:
182 # {
183 # 'test_mapping_file_path': {
184 # 'group_name' : [
185 # 'name': 'module_name',
186 # ],
187 # }
188 # }
189 for test_mapping in test_mappings.values():
190 for group in test_mapping.values():
191 for entry in group:
192 module_name = entry.get('name', None)
193
194 if not module_name:
195 continue
196
197 file_patterns = entry.get('file_patterns')
198 if not file_patterns:
199 modules.add(module_name)
200 continue
201
202 if matches_file_patterns(file_patterns, changed_files):
203 modules.add(module_name)
204 continue
205
206 return modules
207
208
209# TODO(lucafarsi): Share this logic with the original logic in
210# test_mapping_test_retriever.py
211def matches_file_patterns(
Luca Farsi88feffc2024-02-13 12:09:08 -0800212 file_patterns: list[set], changed_files: set[str]
Luca Farsi5717d6f2023-12-28 15:09:28 -0800213) -> bool:
214 for changed_file in changed_files:
215 for pattern in file_patterns:
216 if re.search(pattern, changed_file):
217 return True
218
219 return False
220
221
Luca Farsib559eef2024-01-17 16:14:55 -0800222def zip_build_outputs(
Luca Farsi88feffc2024-02-13 12:09:08 -0800223 modules_to_build: set[str], dist_dir: str, target_release: str
Luca Farsib559eef2024-01-17 16:14:55 -0800224):
Luca Farsi5717d6f2023-12-28 15:09:28 -0800225 src_top = os.environ.get('TOP', os.getcwd())
226
227 # Call dumpvars to get the necessary things.
228 # TODO(lucafarsi): Don't call soong_ui 4 times for this, --dumpvars-mode can
229 # do it but it requires parsing.
Luca Farsi88feffc2024-02-13 12:09:08 -0800230 host_out_testcases = pathlib.Path(
231 get_soong_var('HOST_OUT_TESTCASES', target_release)
232 )
233 target_out_testcases = pathlib.Path(
234 get_soong_var('TARGET_OUT_TESTCASES', target_release)
235 )
236 product_out = pathlib.Path(get_soong_var('PRODUCT_OUT', target_release))
237 soong_host_out = pathlib.Path(get_soong_var('SOONG_HOST_OUT', target_release))
238 host_out = pathlib.Path(get_soong_var('HOST_OUT', target_release))
Luca Farsi5717d6f2023-12-28 15:09:28 -0800239
240 # Call the class to package the outputs.
241 # TODO(lucafarsi): Move this code into a replaceable class.
242 host_paths = []
243 target_paths = []
Luca Farsi88feffc2024-02-13 12:09:08 -0800244 host_config_files = []
245 target_config_files = []
Luca Farsi5717d6f2023-12-28 15:09:28 -0800246 for module in modules_to_build:
247 host_path = os.path.join(host_out_testcases, module)
248 if os.path.exists(host_path):
249 host_paths.append(host_path)
Luca Farsi88feffc2024-02-13 12:09:08 -0800250 collect_config_files(src_top, host_path, host_config_files)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800251
252 target_path = os.path.join(target_out_testcases, module)
253 if os.path.exists(target_path):
254 target_paths.append(target_path)
Luca Farsi88feffc2024-02-13 12:09:08 -0800255 collect_config_files(src_top, target_path, target_config_files)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800256
Luca Farsi88feffc2024-02-13 12:09:08 -0800257 zip_test_configs_zips(
258 dist_dir, host_out, product_out, host_config_files, target_config_files
259 )
260
261 zip_command = base_zip_command(host_out, dist_dir, 'general-tests.zip')
Luca Farsi5717d6f2023-12-28 15:09:28 -0800262
263 # Add host testcases.
264 zip_command.append('-C')
265 zip_command.append(os.path.join(src_top, soong_host_out))
266 zip_command.append('-P')
267 zip_command.append('host/')
268 for path in host_paths:
269 zip_command.append('-D')
270 zip_command.append(path)
271
272 # Add target testcases.
273 zip_command.append('-C')
274 zip_command.append(os.path.join(src_top, product_out))
275 zip_command.append('-P')
276 zip_command.append('target')
277 for path in target_paths:
278 zip_command.append('-D')
279 zip_command.append(path)
280
281 # TODO(lucafarsi): Push this logic into a general-tests-minimal build command
282 # Add necessary tools. These are also hardcoded in general-tests.mk.
283 framework_path = os.path.join(soong_host_out, 'framework')
284
285 zip_command.append('-C')
286 zip_command.append(framework_path)
287 zip_command.append('-P')
288 zip_command.append('host/tools')
289 zip_command.append('-f')
290 zip_command.append(os.path.join(framework_path, 'cts-tradefed.jar'))
291 zip_command.append('-f')
292 zip_command.append(
293 os.path.join(framework_path, 'compatibility-host-util.jar')
294 )
295 zip_command.append('-f')
296 zip_command.append(os.path.join(framework_path, 'vts-tradefed.jar'))
297
Luca Farsib559eef2024-01-17 16:14:55 -0800298 run_command(zip_command, print_output=True)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800299
300
Luca Farsi88feffc2024-02-13 12:09:08 -0800301def collect_config_files(
302 src_top: pathlib.Path, root_dir: pathlib.Path, config_files: list[str]
303):
304 for root, dirs, files in os.walk(os.path.join(src_top, root_dir)):
305 for file in files:
306 if file.endswith('.config'):
307 config_files.append(os.path.join(root_dir, file))
308
309
310def base_zip_command(
311 host_out: pathlib.Path, dist_dir: pathlib.Path, name: str
312) -> list[str]:
313 return [
314 'time',
315 os.path.join(host_out, 'bin', 'soong_zip'),
316 '-d',
317 '-o',
318 os.path.join(dist_dir, name),
319 ]
320
321
322# generate general-tests_configs.zip which contains all of the .config files
323# that were built and general-tests_list.zip which contains a text file which
324# lists all of the .config files that are in general-tests_configs.zip.
325#
326# general-tests_comfigs.zip is organized as follows:
327# /
328# host/
329# testcases/
330# test_1.config
331# test_2.config
332# ...
333# target/
334# testcases/
335# test_1.config
336# test_2.config
337# ...
338#
339# So the process is we write out the paths to all the host config files into one
340# file and all the paths to the target config files in another. We also write
341# the paths to all the config files into a third file to use for
342# general-tests_list.zip.
343def zip_test_configs_zips(
344 dist_dir: pathlib.Path,
345 host_out: pathlib.Path,
346 product_out: pathlib.Path,
347 host_config_files: list[str],
348 target_config_files: list[str],
349):
350 with open(
351 os.path.join(host_out, 'host_general-tests_list'), 'w'
352 ) as host_list_file, open(
353 os.path.join(product_out, 'target_general-tests_list'), 'w'
354 ) as target_list_file, open(
355 os.path.join(host_out, 'general-tests_list'), 'w'
356 ) as list_file:
357
358 for config_file in host_config_files:
359 host_list_file.write(config_file + '\n')
360 list_file.write('host/' + os.path.relpath(config_file, host_out) + '\n')
361
362 for config_file in target_config_files:
363 target_list_file.write(config_file + '\n')
364 list_file.write(
365 'target/' + os.path.relpath(config_file, product_out) + '\n'
366 )
367
368 tests_config_zip_command = base_zip_command(
369 host_out, dist_dir, 'general-tests_configs.zip'
370 )
371 tests_config_zip_command.append('-P')
372 tests_config_zip_command.append('host')
373 tests_config_zip_command.append('-C')
374 tests_config_zip_command.append(host_out)
375 tests_config_zip_command.append('-l')
376 tests_config_zip_command.append(
377 os.path.join(host_out, 'host_general-tests_list')
378 )
379 tests_config_zip_command.append('-P')
380 tests_config_zip_command.append('target')
381 tests_config_zip_command.append('-C')
382 tests_config_zip_command.append(product_out)
383 tests_config_zip_command.append('-l')
384 tests_config_zip_command.append(
385 os.path.join(product_out, 'target_general-tests_list')
386 )
387 run_command(tests_config_zip_command, print_output=True)
388
389 tests_list_zip_command = base_zip_command(
390 host_out, dist_dir, 'general-tests_list.zip'
391 )
392 tests_list_zip_command.append('-C')
393 tests_list_zip_command.append(host_out)
394 tests_list_zip_command.append('-f')
395 tests_list_zip_command.append(os.path.join(host_out, 'general-tests_list'))
396 run_command(tests_list_zip_command, print_output=True)
397
398
Luca Farsib559eef2024-01-17 16:14:55 -0800399def get_soong_var(var: str, target_release: str) -> str:
400 new_env = os.environ.copy()
401 new_env['TARGET_RELEASE'] = target_release
402
Luca Farsi5717d6f2023-12-28 15:09:28 -0800403 value = run_command(
Luca Farsib559eef2024-01-17 16:14:55 -0800404 ['./build/soong/soong_ui.bash', '--dumpvar-mode', '--abs', var],
405 env=new_env,
Luca Farsi5717d6f2023-12-28 15:09:28 -0800406 ).strip()
407 if not value:
408 raise RuntimeError('Necessary soong variable ' + var + ' not found.')
409
410 return value
411
412
Luca Farsi11767d52024-03-07 13:33:57 -0800413def main(argv, extra_targets: set[str]):
414 build_test_suites(argv, extra_targets)