blob: 5798f2b7414495546dd4e3dc766dc6a7cb489399 [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)
Luca Farsi5722c942024-02-21 12:09:10 -080083 # When not building general-tests we also have to build the general tests
84 # shared libs.
85 build_command.append('general-tests-shared-libs')
Luca Farsi5717d6f2023-12-28 15:09:28 -080086
Luca Farsib559eef2024-01-17 16:14:55 -080087 run_command(build_command, print_output=True)
Luca Farsi5717d6f2023-12-28 15:09:28 -080088
Luca Farsib559eef2024-01-17 16:14:55 -080089 zip_build_outputs(modules_to_build, args.dist_dir, args.target_release)
Luca Farsi5717d6f2023-12-28 15:09:28 -080090
91
92def base_build_command(args: argparse.Namespace) -> list:
93 build_command = []
94 build_command.append('time')
95 build_command.append('./build/soong/soong_ui.bash')
96 build_command.append('--make-mode')
97 build_command.append('dist')
98 build_command.append('DIST_DIR=' + args.dist_dir)
99 build_command.append('TARGET_PRODUCT=' + args.target_product)
100 build_command.append('TARGET_RELEASE=' + args.target_release)
Luca Farsi212d3862024-01-12 11:22:06 -0800101 if args.with_dexpreopt_boot_img_and_system_server_only:
102 build_command.append('WITH_DEXPREOPT_BOOT_IMG_AND_SYSTEM_SERVER_ONLY=true')
Luca Farsi5717d6f2023-12-28 15:09:28 -0800103 build_command.extend(args.extra_targets)
104
105 return build_command
106
107
Luca Farsib559eef2024-01-17 16:14:55 -0800108def run_command(
109 args: list[str],
Luca Farsi88feffc2024-02-13 12:09:08 -0800110 env: dict[str, str] = os.environ,
Luca Farsib559eef2024-01-17 16:14:55 -0800111 print_output: bool = False,
112) -> str:
Luca Farsi5717d6f2023-12-28 15:09:28 -0800113 result = subprocess.run(
114 args=args,
115 text=True,
116 capture_output=True,
117 check=False,
Luca Farsib559eef2024-01-17 16:14:55 -0800118 env=env,
Luca Farsi5717d6f2023-12-28 15:09:28 -0800119 )
120 # If the process failed, print its stdout and propagate the exception.
121 if not result.returncode == 0:
122 print('Build command failed! output:')
123 print('stdout: ' + result.stdout)
124 print('stderr: ' + result.stderr)
125
126 result.check_returncode()
Luca Farsib559eef2024-01-17 16:14:55 -0800127
128 if print_output:
129 print(result.stdout)
130
Luca Farsi5717d6f2023-12-28 15:09:28 -0800131 return result.stdout
132
133
134def find_modules_to_build(
Luca Farsi88feffc2024-02-13 12:09:08 -0800135 change_info: pathlib.Path, extra_required_modules: list[str]
136) -> set[str]:
Luca Farsi5717d6f2023-12-28 15:09:28 -0800137 changed_files = find_changed_files(change_info)
138
139 test_mappings = test_mapping_module_retriever.GetTestMappings(
140 changed_files, set()
141 )
142
143 # Soong_zip is required to generate the output zip so always build it.
144 modules_to_build = set(REQUIRED_MODULES)
145 if extra_required_modules:
146 modules_to_build.update(extra_required_modules)
147
148 modules_to_build.update(find_affected_modules(test_mappings, changed_files))
149
150 return modules_to_build
151
152
Luca Farsi88feffc2024-02-13 12:09:08 -0800153def find_changed_files(change_info: pathlib.Path) -> set[str]:
Luca Farsi5717d6f2023-12-28 15:09:28 -0800154 with open(change_info) as change_info_file:
155 change_info_contents = json.load(change_info_file)
156
157 changed_files = set()
158
159 for change in change_info_contents['changes']:
160 project_path = change.get('projectPath') + '/'
161
162 for revision in change.get('revisions'):
163 for file_info in revision.get('fileInfos'):
164 changed_files.add(project_path + file_info.get('path'))
165
166 return changed_files
167
168
169def find_affected_modules(
Luca Farsi88feffc2024-02-13 12:09:08 -0800170 test_mappings: dict[str, Any], changed_files: set[str]
171) -> set[str]:
Luca Farsi5717d6f2023-12-28 15:09:28 -0800172 modules = set()
173
174 # The test_mappings object returned by GetTestMappings is organized as
175 # follows:
176 # {
177 # 'test_mapping_file_path': {
178 # 'group_name' : [
179 # 'name': 'module_name',
180 # ],
181 # }
182 # }
183 for test_mapping in test_mappings.values():
184 for group in test_mapping.values():
185 for entry in group:
186 module_name = entry.get('name', None)
187
188 if not module_name:
189 continue
190
191 file_patterns = entry.get('file_patterns')
192 if not file_patterns:
193 modules.add(module_name)
194 continue
195
196 if matches_file_patterns(file_patterns, changed_files):
197 modules.add(module_name)
198 continue
199
200 return modules
201
202
203# TODO(lucafarsi): Share this logic with the original logic in
204# test_mapping_test_retriever.py
205def matches_file_patterns(
Luca Farsi88feffc2024-02-13 12:09:08 -0800206 file_patterns: list[set], changed_files: set[str]
Luca Farsi5717d6f2023-12-28 15:09:28 -0800207) -> bool:
208 for changed_file in changed_files:
209 for pattern in file_patterns:
210 if re.search(pattern, changed_file):
211 return True
212
213 return False
214
215
Luca Farsib559eef2024-01-17 16:14:55 -0800216def zip_build_outputs(
Luca Farsi88feffc2024-02-13 12:09:08 -0800217 modules_to_build: set[str], dist_dir: str, target_release: str
Luca Farsib559eef2024-01-17 16:14:55 -0800218):
Luca Farsi5717d6f2023-12-28 15:09:28 -0800219 src_top = os.environ.get('TOP', os.getcwd())
220
221 # Call dumpvars to get the necessary things.
222 # TODO(lucafarsi): Don't call soong_ui 4 times for this, --dumpvars-mode can
223 # do it but it requires parsing.
Luca Farsi88feffc2024-02-13 12:09:08 -0800224 host_out_testcases = pathlib.Path(
225 get_soong_var('HOST_OUT_TESTCASES', target_release)
226 )
227 target_out_testcases = pathlib.Path(
228 get_soong_var('TARGET_OUT_TESTCASES', target_release)
229 )
230 product_out = pathlib.Path(get_soong_var('PRODUCT_OUT', target_release))
231 soong_host_out = pathlib.Path(get_soong_var('SOONG_HOST_OUT', target_release))
232 host_out = pathlib.Path(get_soong_var('HOST_OUT', target_release))
Luca Farsi5717d6f2023-12-28 15:09:28 -0800233
234 # Call the class to package the outputs.
235 # TODO(lucafarsi): Move this code into a replaceable class.
236 host_paths = []
237 target_paths = []
Luca Farsi88feffc2024-02-13 12:09:08 -0800238 host_config_files = []
239 target_config_files = []
Luca Farsi5717d6f2023-12-28 15:09:28 -0800240 for module in modules_to_build:
241 host_path = os.path.join(host_out_testcases, module)
242 if os.path.exists(host_path):
243 host_paths.append(host_path)
Luca Farsi88feffc2024-02-13 12:09:08 -0800244 collect_config_files(src_top, host_path, host_config_files)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800245
246 target_path = os.path.join(target_out_testcases, module)
247 if os.path.exists(target_path):
248 target_paths.append(target_path)
Luca Farsi88feffc2024-02-13 12:09:08 -0800249 collect_config_files(src_top, target_path, target_config_files)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800250
Luca Farsi88feffc2024-02-13 12:09:08 -0800251 zip_test_configs_zips(
252 dist_dir, host_out, product_out, host_config_files, target_config_files
253 )
254
255 zip_command = base_zip_command(host_out, dist_dir, 'general-tests.zip')
Luca Farsi5717d6f2023-12-28 15:09:28 -0800256
257 # Add host testcases.
258 zip_command.append('-C')
259 zip_command.append(os.path.join(src_top, soong_host_out))
260 zip_command.append('-P')
261 zip_command.append('host/')
262 for path in host_paths:
263 zip_command.append('-D')
264 zip_command.append(path)
265
266 # Add target testcases.
267 zip_command.append('-C')
268 zip_command.append(os.path.join(src_top, product_out))
269 zip_command.append('-P')
270 zip_command.append('target')
271 for path in target_paths:
272 zip_command.append('-D')
273 zip_command.append(path)
274
275 # TODO(lucafarsi): Push this logic into a general-tests-minimal build command
276 # Add necessary tools. These are also hardcoded in general-tests.mk.
277 framework_path = os.path.join(soong_host_out, 'framework')
278
279 zip_command.append('-C')
280 zip_command.append(framework_path)
281 zip_command.append('-P')
282 zip_command.append('host/tools')
283 zip_command.append('-f')
284 zip_command.append(os.path.join(framework_path, 'cts-tradefed.jar'))
285 zip_command.append('-f')
286 zip_command.append(
287 os.path.join(framework_path, 'compatibility-host-util.jar')
288 )
289 zip_command.append('-f')
290 zip_command.append(os.path.join(framework_path, 'vts-tradefed.jar'))
291
Luca Farsib559eef2024-01-17 16:14:55 -0800292 run_command(zip_command, print_output=True)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800293
294
Luca Farsi88feffc2024-02-13 12:09:08 -0800295def collect_config_files(
296 src_top: pathlib.Path, root_dir: pathlib.Path, config_files: list[str]
297):
298 for root, dirs, files in os.walk(os.path.join(src_top, root_dir)):
299 for file in files:
300 if file.endswith('.config'):
301 config_files.append(os.path.join(root_dir, file))
302
303
304def base_zip_command(
305 host_out: pathlib.Path, dist_dir: pathlib.Path, name: str
306) -> list[str]:
307 return [
308 'time',
309 os.path.join(host_out, 'bin', 'soong_zip'),
310 '-d',
311 '-o',
312 os.path.join(dist_dir, name),
313 ]
314
315
316# generate general-tests_configs.zip which contains all of the .config files
317# that were built and general-tests_list.zip which contains a text file which
318# lists all of the .config files that are in general-tests_configs.zip.
319#
320# general-tests_comfigs.zip is organized as follows:
321# /
322# host/
323# testcases/
324# test_1.config
325# test_2.config
326# ...
327# target/
328# testcases/
329# test_1.config
330# test_2.config
331# ...
332#
333# So the process is we write out the paths to all the host config files into one
334# file and all the paths to the target config files in another. We also write
335# the paths to all the config files into a third file to use for
336# general-tests_list.zip.
337def zip_test_configs_zips(
338 dist_dir: pathlib.Path,
339 host_out: pathlib.Path,
340 product_out: pathlib.Path,
341 host_config_files: list[str],
342 target_config_files: list[str],
343):
344 with open(
345 os.path.join(host_out, 'host_general-tests_list'), 'w'
346 ) as host_list_file, open(
347 os.path.join(product_out, 'target_general-tests_list'), 'w'
348 ) as target_list_file, open(
349 os.path.join(host_out, 'general-tests_list'), 'w'
350 ) as list_file:
351
352 for config_file in host_config_files:
353 host_list_file.write(config_file + '\n')
354 list_file.write('host/' + os.path.relpath(config_file, host_out) + '\n')
355
356 for config_file in target_config_files:
357 target_list_file.write(config_file + '\n')
358 list_file.write(
359 'target/' + os.path.relpath(config_file, product_out) + '\n'
360 )
361
362 tests_config_zip_command = base_zip_command(
363 host_out, dist_dir, 'general-tests_configs.zip'
364 )
365 tests_config_zip_command.append('-P')
366 tests_config_zip_command.append('host')
367 tests_config_zip_command.append('-C')
368 tests_config_zip_command.append(host_out)
369 tests_config_zip_command.append('-l')
370 tests_config_zip_command.append(
371 os.path.join(host_out, 'host_general-tests_list')
372 )
373 tests_config_zip_command.append('-P')
374 tests_config_zip_command.append('target')
375 tests_config_zip_command.append('-C')
376 tests_config_zip_command.append(product_out)
377 tests_config_zip_command.append('-l')
378 tests_config_zip_command.append(
379 os.path.join(product_out, 'target_general-tests_list')
380 )
381 run_command(tests_config_zip_command, print_output=True)
382
383 tests_list_zip_command = base_zip_command(
384 host_out, dist_dir, 'general-tests_list.zip'
385 )
386 tests_list_zip_command.append('-C')
387 tests_list_zip_command.append(host_out)
388 tests_list_zip_command.append('-f')
389 tests_list_zip_command.append(os.path.join(host_out, 'general-tests_list'))
390 run_command(tests_list_zip_command, print_output=True)
391
392
Luca Farsib559eef2024-01-17 16:14:55 -0800393def get_soong_var(var: str, target_release: str) -> str:
394 new_env = os.environ.copy()
395 new_env['TARGET_RELEASE'] = target_release
396
Luca Farsi5717d6f2023-12-28 15:09:28 -0800397 value = run_command(
Luca Farsib559eef2024-01-17 16:14:55 -0800398 ['./build/soong/soong_ui.bash', '--dumpvar-mode', '--abs', var],
399 env=new_env,
Luca Farsi5717d6f2023-12-28 15:09:28 -0800400 ).strip()
401 if not value:
402 raise RuntimeError('Necessary soong variable ' + var + ' not found.')
403
404 return value
405
406
407def main(argv):
408 build_test_suites(sys.argv)