blob: 0c1d2110a5de91f04ff987502cd54b3d9834f76b [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
28from typing import Any, Dict, Set, Text
29
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
42 if not args.change_info:
43 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
72 run_command(build_command)
73
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
84 run_command(build_command)
85
86 zip_build_outputs(modules_to_build, args.dist_dir)
87
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
105def run_command(args: list[str]) -> str:
106 result = subprocess.run(
107 args=args,
108 text=True,
109 capture_output=True,
110 check=False,
111 )
112 # If the process failed, print its stdout and propagate the exception.
113 if not result.returncode == 0:
114 print('Build command failed! output:')
115 print('stdout: ' + result.stdout)
116 print('stderr: ' + result.stderr)
117
118 result.check_returncode()
119 return result.stdout
120
121
122def find_modules_to_build(
123 change_info: pathlib.Path, extra_required_modules: list[Text]
124) -> Set[Text]:
125 changed_files = find_changed_files(change_info)
126
127 test_mappings = test_mapping_module_retriever.GetTestMappings(
128 changed_files, set()
129 )
130
131 # Soong_zip is required to generate the output zip so always build it.
132 modules_to_build = set(REQUIRED_MODULES)
133 if extra_required_modules:
134 modules_to_build.update(extra_required_modules)
135
136 modules_to_build.update(find_affected_modules(test_mappings, changed_files))
137
138 return modules_to_build
139
140
141def find_changed_files(change_info: pathlib.Path) -> Set[Text]:
142 with open(change_info) as change_info_file:
143 change_info_contents = json.load(change_info_file)
144
145 changed_files = set()
146
147 for change in change_info_contents['changes']:
148 project_path = change.get('projectPath') + '/'
149
150 for revision in change.get('revisions'):
151 for file_info in revision.get('fileInfos'):
152 changed_files.add(project_path + file_info.get('path'))
153
154 return changed_files
155
156
157def find_affected_modules(
158 test_mappings: Dict[str, Any], changed_files: Set[Text]
159) -> Set[Text]:
160 modules = set()
161
162 # The test_mappings object returned by GetTestMappings is organized as
163 # follows:
164 # {
165 # 'test_mapping_file_path': {
166 # 'group_name' : [
167 # 'name': 'module_name',
168 # ],
169 # }
170 # }
171 for test_mapping in test_mappings.values():
172 for group in test_mapping.values():
173 for entry in group:
174 module_name = entry.get('name', None)
175
176 if not module_name:
177 continue
178
179 file_patterns = entry.get('file_patterns')
180 if not file_patterns:
181 modules.add(module_name)
182 continue
183
184 if matches_file_patterns(file_patterns, changed_files):
185 modules.add(module_name)
186 continue
187
188 return modules
189
190
191# TODO(lucafarsi): Share this logic with the original logic in
192# test_mapping_test_retriever.py
193def matches_file_patterns(
194 file_patterns: list[Text], changed_files: Set[Text]
195) -> bool:
196 for changed_file in changed_files:
197 for pattern in file_patterns:
198 if re.search(pattern, changed_file):
199 return True
200
201 return False
202
203
204def zip_build_outputs(modules_to_build: Set[Text], dist_dir: Text):
205 src_top = os.environ.get('TOP', os.getcwd())
206
207 # Call dumpvars to get the necessary things.
208 # TODO(lucafarsi): Don't call soong_ui 4 times for this, --dumpvars-mode can
209 # do it but it requires parsing.
210 host_out_testcases = get_soong_var('HOST_OUT_TESTCASES')
211 target_out_testcases = get_soong_var('TARGET_OUT_TESTCASES')
212 product_out = get_soong_var('PRODUCT_OUT')
213 soong_host_out = get_soong_var('SOONG_HOST_OUT')
214 host_out = get_soong_var('HOST_OUT')
215
216 # Call the class to package the outputs.
217 # TODO(lucafarsi): Move this code into a replaceable class.
218 host_paths = []
219 target_paths = []
220 for module in modules_to_build:
221 host_path = os.path.join(host_out_testcases, module)
222 if os.path.exists(host_path):
223 host_paths.append(host_path)
224
225 target_path = os.path.join(target_out_testcases, module)
226 if os.path.exists(target_path):
227 target_paths.append(target_path)
228
229 zip_command = ['time', os.path.join(host_out, 'bin', 'soong_zip')]
230
231 # Add host testcases.
232 zip_command.append('-C')
233 zip_command.append(os.path.join(src_top, soong_host_out))
234 zip_command.append('-P')
235 zip_command.append('host/')
236 for path in host_paths:
237 zip_command.append('-D')
238 zip_command.append(path)
239
240 # Add target testcases.
241 zip_command.append('-C')
242 zip_command.append(os.path.join(src_top, product_out))
243 zip_command.append('-P')
244 zip_command.append('target')
245 for path in target_paths:
246 zip_command.append('-D')
247 zip_command.append(path)
248
249 # TODO(lucafarsi): Push this logic into a general-tests-minimal build command
250 # Add necessary tools. These are also hardcoded in general-tests.mk.
251 framework_path = os.path.join(soong_host_out, 'framework')
252
253 zip_command.append('-C')
254 zip_command.append(framework_path)
255 zip_command.append('-P')
256 zip_command.append('host/tools')
257 zip_command.append('-f')
258 zip_command.append(os.path.join(framework_path, 'cts-tradefed.jar'))
259 zip_command.append('-f')
260 zip_command.append(
261 os.path.join(framework_path, 'compatibility-host-util.jar')
262 )
263 zip_command.append('-f')
264 zip_command.append(os.path.join(framework_path, 'vts-tradefed.jar'))
265
266 # Zip to the DIST dir.
267 zip_command.append('-o')
268 zip_command.append(os.path.join(dist_dir, 'general-tests.zip'))
269
270 run_command(zip_command)
271
272
273def get_soong_var(var: str) -> str:
274 value = run_command(
275 ['./build/soong/soong_ui.bash', '--dumpvar-mode', '--abs', var]
276 ).strip()
277 if not value:
278 raise RuntimeError('Necessary soong variable ' + var + ' not found.')
279
280 return value
281
282
283def main(argv):
284 build_test_suites(sys.argv)