xref: /aosp_15_r20/build/bazel/ci/rbc_dashboard.py (revision 7594170e27e0732bc44b93d1440d87a54b6ffe7c)
1*7594170eSAndroid Build Coastguard Worker#!/usr/bin/env python3
2*7594170eSAndroid Build Coastguard Worker"""Generates a dashboard for the current RBC product/board config conversion status."""
3*7594170eSAndroid Build Coastguard Worker# pylint: disable=line-too-long
4*7594170eSAndroid Build Coastguard Worker
5*7594170eSAndroid Build Coastguard Workerimport argparse
6*7594170eSAndroid Build Coastguard Workerimport asyncio
7*7594170eSAndroid Build Coastguard Workerimport dataclasses
8*7594170eSAndroid Build Coastguard Workerimport datetime
9*7594170eSAndroid Build Coastguard Workerimport itertools
10*7594170eSAndroid Build Coastguard Workerimport os
11*7594170eSAndroid Build Coastguard Workerimport re
12*7594170eSAndroid Build Coastguard Workerimport shutil
13*7594170eSAndroid Build Coastguard Workerimport socket
14*7594170eSAndroid Build Coastguard Workerimport subprocess
15*7594170eSAndroid Build Coastguard Workerimport sys
16*7594170eSAndroid Build Coastguard Workerimport time
17*7594170eSAndroid Build Coastguard Workerfrom typing import List, Tuple
18*7594170eSAndroid Build Coastguard Workerimport xml.etree.ElementTree as ET
19*7594170eSAndroid Build Coastguard Worker
20*7594170eSAndroid Build Coastguard Worker_PRODUCT_REGEX = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*)(?:(?:-(trunk|trunk_staging|next))?-(user|userdebug|eng))?')
21*7594170eSAndroid Build Coastguard Worker
22*7594170eSAndroid Build Coastguard Worker_ALREADY_FAILING_PRODUCTS = [
23*7594170eSAndroid Build Coastguard Worker  "aosp_cf_x86_64_tv",
24*7594170eSAndroid Build Coastguard Worker  "aosp_cf_x86_tv",
25*7594170eSAndroid Build Coastguard Worker  "aosp_husky_61_pgagnostic",
26*7594170eSAndroid Build Coastguard Worker  "aosp_shiba_61_pgagnostic",
27*7594170eSAndroid Build Coastguard Worker]
28*7594170eSAndroid Build Coastguard Worker
29*7594170eSAndroid Build Coastguard Worker@dataclasses.dataclass(frozen=True)
30*7594170eSAndroid Build Coastguard Workerclass Product:
31*7594170eSAndroid Build Coastguard Worker  """Represents a TARGET_PRODUCT and TARGET_BUILD_VARIANT."""
32*7594170eSAndroid Build Coastguard Worker  product: str
33*7594170eSAndroid Build Coastguard Worker  release: str
34*7594170eSAndroid Build Coastguard Worker  variant: str
35*7594170eSAndroid Build Coastguard Worker
36*7594170eSAndroid Build Coastguard Worker  def __post_init__(self):
37*7594170eSAndroid Build Coastguard Worker    if not _PRODUCT_REGEX.match(str(self)):
38*7594170eSAndroid Build Coastguard Worker      raise ValueError(f'Invalid product name: {self}')
39*7594170eSAndroid Build Coastguard Worker
40*7594170eSAndroid Build Coastguard Worker  def __str__(self):
41*7594170eSAndroid Build Coastguard Worker    return self.product + '-' + self.release + '-' + self.variant
42*7594170eSAndroid Build Coastguard Worker
43*7594170eSAndroid Build Coastguard Worker
44*7594170eSAndroid Build Coastguard Worker@dataclasses.dataclass(frozen=True)
45*7594170eSAndroid Build Coastguard Workerclass ProductResult:
46*7594170eSAndroid Build Coastguard Worker  product: Product
47*7594170eSAndroid Build Coastguard Worker  baseline_success: bool
48*7594170eSAndroid Build Coastguard Worker  product_success: bool
49*7594170eSAndroid Build Coastguard Worker  product_has_diffs: bool
50*7594170eSAndroid Build Coastguard Worker
51*7594170eSAndroid Build Coastguard Worker  def success(self) -> bool:
52*7594170eSAndroid Build Coastguard Worker    return not self.baseline_success or (
53*7594170eSAndroid Build Coastguard Worker        self.product_success
54*7594170eSAndroid Build Coastguard Worker        and not self.product_has_diffs)
55*7594170eSAndroid Build Coastguard Worker
56*7594170eSAndroid Build Coastguard Worker
57*7594170eSAndroid Build Coastguard Worker@dataclasses.dataclass(frozen=True)
58*7594170eSAndroid Build Coastguard Workerclass Directories:
59*7594170eSAndroid Build Coastguard Worker  out: str
60*7594170eSAndroid Build Coastguard Worker  out_baseline: str
61*7594170eSAndroid Build Coastguard Worker  out_product: str
62*7594170eSAndroid Build Coastguard Worker  results: str
63*7594170eSAndroid Build Coastguard Worker
64*7594170eSAndroid Build Coastguard Worker
65*7594170eSAndroid Build Coastguard Workerdef get_top() -> str:
66*7594170eSAndroid Build Coastguard Worker  path = '.'
67*7594170eSAndroid Build Coastguard Worker  while not os.path.isfile(os.path.join(path, 'build/soong/soong_ui.bash')):
68*7594170eSAndroid Build Coastguard Worker    if os.path.abspath(path) == '/':
69*7594170eSAndroid Build Coastguard Worker      sys.exit('Could not find android source tree root.')
70*7594170eSAndroid Build Coastguard Worker    path = os.path.join(path, '..')
71*7594170eSAndroid Build Coastguard Worker  return os.path.abspath(path)
72*7594170eSAndroid Build Coastguard Worker
73*7594170eSAndroid Build Coastguard Worker
74*7594170eSAndroid Build Coastguard Workerdef get_build_var(variable, product: Product) -> str:
75*7594170eSAndroid Build Coastguard Worker  """Returns the result of the shell command get_build_var."""
76*7594170eSAndroid Build Coastguard Worker  env = {
77*7594170eSAndroid Build Coastguard Worker      **os.environ,
78*7594170eSAndroid Build Coastguard Worker      'TARGET_PRODUCT': product.product,
79*7594170eSAndroid Build Coastguard Worker      'TARGET_RELEASE': product.release,
80*7594170eSAndroid Build Coastguard Worker      'TARGET_BUILD_VARIANT': product.variant,
81*7594170eSAndroid Build Coastguard Worker  }
82*7594170eSAndroid Build Coastguard Worker  return subprocess.check_output([
83*7594170eSAndroid Build Coastguard Worker      'build/soong/soong_ui.bash',
84*7594170eSAndroid Build Coastguard Worker      '--dumpvar-mode',
85*7594170eSAndroid Build Coastguard Worker      variable
86*7594170eSAndroid Build Coastguard Worker  ], env=env, text=True).strip()
87*7594170eSAndroid Build Coastguard Worker
88*7594170eSAndroid Build Coastguard Worker
89*7594170eSAndroid Build Coastguard Workerasync def run_jailed_command(args: List[str], out_dir: str, env=None) -> bool:
90*7594170eSAndroid Build Coastguard Worker  """Runs a command, saves its output to out_dir/build.log, and returns if it succeeded."""
91*7594170eSAndroid Build Coastguard Worker  with open(os.path.join(out_dir, 'build.log'), 'wb') as f:
92*7594170eSAndroid Build Coastguard Worker    result = await asyncio.create_subprocess_exec(
93*7594170eSAndroid Build Coastguard Worker        'prebuilts/build-tools/linux-x86/bin/nsjail',
94*7594170eSAndroid Build Coastguard Worker        '-q',
95*7594170eSAndroid Build Coastguard Worker        '--cwd',
96*7594170eSAndroid Build Coastguard Worker        os.getcwd(),
97*7594170eSAndroid Build Coastguard Worker        '-e',
98*7594170eSAndroid Build Coastguard Worker        '-B',
99*7594170eSAndroid Build Coastguard Worker        '/',
100*7594170eSAndroid Build Coastguard Worker        '-B',
101*7594170eSAndroid Build Coastguard Worker        f'{os.path.abspath(out_dir)}:{os.path.abspath("out")}',
102*7594170eSAndroid Build Coastguard Worker        '--time_limit',
103*7594170eSAndroid Build Coastguard Worker        '0',
104*7594170eSAndroid Build Coastguard Worker        '--skip_setsid',
105*7594170eSAndroid Build Coastguard Worker        '--keep_caps',
106*7594170eSAndroid Build Coastguard Worker        '--disable_clone_newcgroup',
107*7594170eSAndroid Build Coastguard Worker        '--disable_clone_newnet',
108*7594170eSAndroid Build Coastguard Worker        '--rlimit_as',
109*7594170eSAndroid Build Coastguard Worker        'soft',
110*7594170eSAndroid Build Coastguard Worker        '--rlimit_core',
111*7594170eSAndroid Build Coastguard Worker        'soft',
112*7594170eSAndroid Build Coastguard Worker        '--rlimit_cpu',
113*7594170eSAndroid Build Coastguard Worker        'soft',
114*7594170eSAndroid Build Coastguard Worker        '--rlimit_fsize',
115*7594170eSAndroid Build Coastguard Worker        'soft',
116*7594170eSAndroid Build Coastguard Worker        '--rlimit_nofile',
117*7594170eSAndroid Build Coastguard Worker        'soft',
118*7594170eSAndroid Build Coastguard Worker        '--proc_rw',
119*7594170eSAndroid Build Coastguard Worker        '--hostname',
120*7594170eSAndroid Build Coastguard Worker        socket.gethostname(),
121*7594170eSAndroid Build Coastguard Worker        '--',
122*7594170eSAndroid Build Coastguard Worker        *args, stdout=f, stderr=subprocess.STDOUT, env=env)
123*7594170eSAndroid Build Coastguard Worker    return await result.wait() == 0
124*7594170eSAndroid Build Coastguard Worker
125*7594170eSAndroid Build Coastguard Worker
126*7594170eSAndroid Build Coastguard Workerasync def run_build(flags: List[str], out_dir: str) -> bool:
127*7594170eSAndroid Build Coastguard Worker  return await run_jailed_command([
128*7594170eSAndroid Build Coastguard Worker      'build/soong/soong_ui.bash',
129*7594170eSAndroid Build Coastguard Worker      '--make-mode',
130*7594170eSAndroid Build Coastguard Worker      *flags,
131*7594170eSAndroid Build Coastguard Worker      '--skip-ninja',
132*7594170eSAndroid Build Coastguard Worker      'nothing'
133*7594170eSAndroid Build Coastguard Worker  ], out_dir)
134*7594170eSAndroid Build Coastguard Worker
135*7594170eSAndroid Build Coastguard Worker
136*7594170eSAndroid Build Coastguard Workerasync def run_config(product: Product, rbc_product: bool, out_dir: str) -> bool:
137*7594170eSAndroid Build Coastguard Worker  """Runs config.mk and saves results to out/rbc_variable_dump.txt."""
138*7594170eSAndroid Build Coastguard Worker  env = {
139*7594170eSAndroid Build Coastguard Worker      'OUT_DIR': 'out',
140*7594170eSAndroid Build Coastguard Worker      'TMPDIR': 'tmp',
141*7594170eSAndroid Build Coastguard Worker      'BUILD_DATETIME_FILE': 'out/build_date.txt',
142*7594170eSAndroid Build Coastguard Worker      'CALLED_FROM_SETUP': 'true',
143*7594170eSAndroid Build Coastguard Worker      'TARGET_PRODUCT': product.product,
144*7594170eSAndroid Build Coastguard Worker      'TARGET_BUILD_VARIANT': product.variant,
145*7594170eSAndroid Build Coastguard Worker      'TARGET_RELEASE': product.release,
146*7594170eSAndroid Build Coastguard Worker      'RBC_PRODUCT_CONFIG': 'true' if rbc_product else '',
147*7594170eSAndroid Build Coastguard Worker      'RBC_DUMP_CONFIG_FILE': 'out/rbc_variable_dump.txt',
148*7594170eSAndroid Build Coastguard Worker  }
149*7594170eSAndroid Build Coastguard Worker  return await run_jailed_command([
150*7594170eSAndroid Build Coastguard Worker      'prebuilts/build-tools/linux-x86/bin/ckati',
151*7594170eSAndroid Build Coastguard Worker      '-f',
152*7594170eSAndroid Build Coastguard Worker      'build/make/core/config.mk'
153*7594170eSAndroid Build Coastguard Worker  ], out_dir, env=env)
154*7594170eSAndroid Build Coastguard Worker
155*7594170eSAndroid Build Coastguard Worker
156*7594170eSAndroid Build Coastguard Workerasync def has_diffs(success: bool, file_pairs: List[Tuple[str]], results_folder: str) -> bool:
157*7594170eSAndroid Build Coastguard Worker  """Returns true if the two out folders provided have differing ninja files."""
158*7594170eSAndroid Build Coastguard Worker  if not success:
159*7594170eSAndroid Build Coastguard Worker    return False
160*7594170eSAndroid Build Coastguard Worker  results = []
161*7594170eSAndroid Build Coastguard Worker  for pair in file_pairs:
162*7594170eSAndroid Build Coastguard Worker    name = 'soong_build.ninja' if re.search('soong/build\.[^.]+\.ninja$', pair[0]) else os.path.basename(pair[0])
163*7594170eSAndroid Build Coastguard Worker    with open(os.path.join(results_folder, name)+'.diff', 'wb') as f:
164*7594170eSAndroid Build Coastguard Worker      results.append((await asyncio.create_subprocess_exec(
165*7594170eSAndroid Build Coastguard Worker          'diff',
166*7594170eSAndroid Build Coastguard Worker          pair[0],
167*7594170eSAndroid Build Coastguard Worker          pair[1],
168*7594170eSAndroid Build Coastguard Worker          stdout=f, stderr=subprocess.STDOUT)).wait())
169*7594170eSAndroid Build Coastguard Worker
170*7594170eSAndroid Build Coastguard Worker  for return_code in await asyncio.gather(*results):
171*7594170eSAndroid Build Coastguard Worker    if return_code != 0:
172*7594170eSAndroid Build Coastguard Worker      return True
173*7594170eSAndroid Build Coastguard Worker  return False
174*7594170eSAndroid Build Coastguard Worker
175*7594170eSAndroid Build Coastguard Worker
176*7594170eSAndroid Build Coastguard Workerdef generate_html_row(num: int, results: ProductResult):
177*7594170eSAndroid Build Coastguard Worker  def generate_status_cell(success: bool, diffs: bool) -> str:
178*7594170eSAndroid Build Coastguard Worker    message = 'Success'
179*7594170eSAndroid Build Coastguard Worker    if diffs:
180*7594170eSAndroid Build Coastguard Worker      message = 'Results differed'
181*7594170eSAndroid Build Coastguard Worker    if not success:
182*7594170eSAndroid Build Coastguard Worker      message = 'Build failed'
183*7594170eSAndroid Build Coastguard Worker    return f'<td style="background-color: {"lightgreen" if success and not diffs else "salmon"}">{message}</td>'
184*7594170eSAndroid Build Coastguard Worker
185*7594170eSAndroid Build Coastguard Worker  product = results.product
186*7594170eSAndroid Build Coastguard Worker  return f'''
187*7594170eSAndroid Build Coastguard Worker  <tr>
188*7594170eSAndroid Build Coastguard Worker    <td>{num}</td>
189*7594170eSAndroid Build Coastguard Worker    <td>{product if results.success() and results.baseline_success else f'<a href="{product}/">{product}</a>'}</td>
190*7594170eSAndroid Build Coastguard Worker    {generate_status_cell(results.baseline_success, False)}
191*7594170eSAndroid Build Coastguard Worker    {generate_status_cell(results.product_success, results.product_has_diffs)}
192*7594170eSAndroid Build Coastguard Worker  </tr>
193*7594170eSAndroid Build Coastguard Worker  '''
194*7594170eSAndroid Build Coastguard Worker
195*7594170eSAndroid Build Coastguard Worker
196*7594170eSAndroid Build Coastguard Workerdef get_branch() -> str:
197*7594170eSAndroid Build Coastguard Worker  try:
198*7594170eSAndroid Build Coastguard Worker    tree = ET.parse('.repo/manifests/default.xml')
199*7594170eSAndroid Build Coastguard Worker    default_tag = tree.getroot().find('default')
200*7594170eSAndroid Build Coastguard Worker    return default_tag.get('remote') + '/' + default_tag.get('revision')
201*7594170eSAndroid Build Coastguard Worker  except Exception as e:  # pylint: disable=broad-except
202*7594170eSAndroid Build Coastguard Worker    # Most likely happens due to .repo not existing on CI
203*7594170eSAndroid Build Coastguard Worker    return 'Unknown'
204*7594170eSAndroid Build Coastguard Worker
205*7594170eSAndroid Build Coastguard Worker
206*7594170eSAndroid Build Coastguard Workerdef cleanup_empty_files(path):
207*7594170eSAndroid Build Coastguard Worker  if os.path.isfile(path):
208*7594170eSAndroid Build Coastguard Worker    if os.path.getsize(path) == 0:
209*7594170eSAndroid Build Coastguard Worker      os.remove(path)
210*7594170eSAndroid Build Coastguard Worker  elif os.path.isdir(path):
211*7594170eSAndroid Build Coastguard Worker    for subfile in os.listdir(path):
212*7594170eSAndroid Build Coastguard Worker      cleanup_empty_files(os.path.join(path, subfile))
213*7594170eSAndroid Build Coastguard Worker    if not os.listdir(path):
214*7594170eSAndroid Build Coastguard Worker      os.rmdir(path)
215*7594170eSAndroid Build Coastguard Worker
216*7594170eSAndroid Build Coastguard Worker
217*7594170eSAndroid Build Coastguard Workerdef dump_files_to_stderr(path):
218*7594170eSAndroid Build Coastguard Worker  if os.path.isfile(path):
219*7594170eSAndroid Build Coastguard Worker    with open(path, 'r') as f:
220*7594170eSAndroid Build Coastguard Worker      print(f'{path}:', file=sys.stderr)
221*7594170eSAndroid Build Coastguard Worker      for line in itertools.islice(f, 200):
222*7594170eSAndroid Build Coastguard Worker        print(line.rstrip('\r\n'), file=sys.stderr)
223*7594170eSAndroid Build Coastguard Worker      if next(f, None) != None:
224*7594170eSAndroid Build Coastguard Worker        print('... Remaining lines skipped ...', file=sys.stderr)
225*7594170eSAndroid Build Coastguard Worker  elif os.path.isdir(path):
226*7594170eSAndroid Build Coastguard Worker    for subfile in os.listdir(path):
227*7594170eSAndroid Build Coastguard Worker      dump_files_to_stderr(os.path.join(path, subfile))
228*7594170eSAndroid Build Coastguard Worker
229*7594170eSAndroid Build Coastguard Worker
230*7594170eSAndroid Build Coastguard Workerasync def test_one_product(product: Product, dirs: Directories) -> ProductResult:
231*7594170eSAndroid Build Coastguard Worker  """Runs the builds and tests for differences for a single product."""
232*7594170eSAndroid Build Coastguard Worker  baseline_success, product_success = await asyncio.gather(
233*7594170eSAndroid Build Coastguard Worker      run_build([
234*7594170eSAndroid Build Coastguard Worker          f'TARGET_PRODUCT={product.product}',
235*7594170eSAndroid Build Coastguard Worker          f'TARGET_RELEASE={product.release}',
236*7594170eSAndroid Build Coastguard Worker          f'TARGET_BUILD_VARIANT={product.variant}',
237*7594170eSAndroid Build Coastguard Worker      ], dirs.out_baseline),
238*7594170eSAndroid Build Coastguard Worker      run_build([
239*7594170eSAndroid Build Coastguard Worker          f'TARGET_PRODUCT={product.product}',
240*7594170eSAndroid Build Coastguard Worker          f'TARGET_RELEASE={product.release}',
241*7594170eSAndroid Build Coastguard Worker          f'TARGET_BUILD_VARIANT={product.variant}',
242*7594170eSAndroid Build Coastguard Worker          'RBC_PRODUCT_CONFIG=1',
243*7594170eSAndroid Build Coastguard Worker      ], dirs.out_product),
244*7594170eSAndroid Build Coastguard Worker  )
245*7594170eSAndroid Build Coastguard Worker
246*7594170eSAndroid Build Coastguard Worker  product_dashboard_folder = os.path.join(dirs.results, str(product))
247*7594170eSAndroid Build Coastguard Worker  os.mkdir(product_dashboard_folder)
248*7594170eSAndroid Build Coastguard Worker  os.mkdir(product_dashboard_folder+'/baseline')
249*7594170eSAndroid Build Coastguard Worker  os.mkdir(product_dashboard_folder+'/product')
250*7594170eSAndroid Build Coastguard Worker
251*7594170eSAndroid Build Coastguard Worker  if not baseline_success:
252*7594170eSAndroid Build Coastguard Worker    shutil.copy2(os.path.join(dirs.out_baseline, 'build.log'),
253*7594170eSAndroid Build Coastguard Worker                 f'{product_dashboard_folder}/baseline/build.log')
254*7594170eSAndroid Build Coastguard Worker  if not product_success:
255*7594170eSAndroid Build Coastguard Worker    shutil.copy2(os.path.join(dirs.out_product, 'build.log'),
256*7594170eSAndroid Build Coastguard Worker                 f'{product_dashboard_folder}/product/build.log')
257*7594170eSAndroid Build Coastguard Worker    add_message = False
258*7594170eSAndroid Build Coastguard Worker    with open(f'{product_dashboard_folder}/product/build.log', 'r') as f:
259*7594170eSAndroid Build Coastguard Worker      if '/out/rbc/' in f.read():
260*7594170eSAndroid Build Coastguard Worker        add_message = True
261*7594170eSAndroid Build Coastguard Worker    if add_message:
262*7594170eSAndroid Build Coastguard Worker      with open(f'{product_dashboard_folder}/product/build.log', 'a') as f:
263*7594170eSAndroid Build Coastguard Worker        f.write(f'\nPaths involving out/rbc are actually under {dirs.out_product}\n')
264*7594170eSAndroid Build Coastguard Worker
265*7594170eSAndroid Build Coastguard Worker  files = [f'build-{product.product}.ninja', f'build-{product.product}-package.ninja', f'soong/build.{product.product}.ninja']
266*7594170eSAndroid Build Coastguard Worker  product_files = [(os.path.join(dirs.out_baseline, x), os.path.join(dirs.out_product, x)) for x in files]
267*7594170eSAndroid Build Coastguard Worker  product_has_diffs = await has_diffs(baseline_success and product_success, product_files, product_dashboard_folder+'/product')
268*7594170eSAndroid Build Coastguard Worker
269*7594170eSAndroid Build Coastguard Worker  # delete files that contain the product name in them to save space,
270*7594170eSAndroid Build Coastguard Worker  # otherwise the ninja files end up filling up the whole harddrive
271*7594170eSAndroid Build Coastguard Worker  for out_folder in [dirs.out_baseline, dirs.out_product]:
272*7594170eSAndroid Build Coastguard Worker    for subfolder in ['', 'soong']:
273*7594170eSAndroid Build Coastguard Worker      folder = os.path.join(out_folder, subfolder)
274*7594170eSAndroid Build Coastguard Worker      for file in os.listdir(folder):
275*7594170eSAndroid Build Coastguard Worker        if os.path.isfile(os.path.join(folder, file)) and product.product in file:
276*7594170eSAndroid Build Coastguard Worker          os.remove(os.path.join(folder, file))
277*7594170eSAndroid Build Coastguard Worker
278*7594170eSAndroid Build Coastguard Worker  cleanup_empty_files(product_dashboard_folder)
279*7594170eSAndroid Build Coastguard Worker
280*7594170eSAndroid Build Coastguard Worker  return ProductResult(product, baseline_success, product_success, product_has_diffs)
281*7594170eSAndroid Build Coastguard Worker
282*7594170eSAndroid Build Coastguard Worker
283*7594170eSAndroid Build Coastguard Workerasync def test_one_product_quick(product: Product, dirs: Directories) -> ProductResult:
284*7594170eSAndroid Build Coastguard Worker  """Runs the builds and tests for differences for a single product."""
285*7594170eSAndroid Build Coastguard Worker  baseline_success, product_success = await asyncio.gather(
286*7594170eSAndroid Build Coastguard Worker      run_config(
287*7594170eSAndroid Build Coastguard Worker          product,
288*7594170eSAndroid Build Coastguard Worker          False,
289*7594170eSAndroid Build Coastguard Worker          dirs.out_baseline),
290*7594170eSAndroid Build Coastguard Worker      run_config(
291*7594170eSAndroid Build Coastguard Worker          product,
292*7594170eSAndroid Build Coastguard Worker          True,
293*7594170eSAndroid Build Coastguard Worker          dirs.out_product),
294*7594170eSAndroid Build Coastguard Worker  )
295*7594170eSAndroid Build Coastguard Worker
296*7594170eSAndroid Build Coastguard Worker  product_dashboard_folder = os.path.join(dirs.results, str(product))
297*7594170eSAndroid Build Coastguard Worker  os.mkdir(product_dashboard_folder)
298*7594170eSAndroid Build Coastguard Worker  os.mkdir(product_dashboard_folder+'/baseline')
299*7594170eSAndroid Build Coastguard Worker  os.mkdir(product_dashboard_folder+'/product')
300*7594170eSAndroid Build Coastguard Worker
301*7594170eSAndroid Build Coastguard Worker  if not baseline_success:
302*7594170eSAndroid Build Coastguard Worker    shutil.copy2(os.path.join(dirs.out_baseline, 'build.log'),
303*7594170eSAndroid Build Coastguard Worker                 f'{product_dashboard_folder}/baseline/build.log')
304*7594170eSAndroid Build Coastguard Worker  if not product_success:
305*7594170eSAndroid Build Coastguard Worker    shutil.copy2(os.path.join(dirs.out_product, 'build.log'),
306*7594170eSAndroid Build Coastguard Worker                 f'{product_dashboard_folder}/product/build.log')
307*7594170eSAndroid Build Coastguard Worker    add_message = False
308*7594170eSAndroid Build Coastguard Worker    with open(f'{product_dashboard_folder}/product/build.log', 'r') as f:
309*7594170eSAndroid Build Coastguard Worker      if '/out/rbc/' in f.read():
310*7594170eSAndroid Build Coastguard Worker        add_message = True
311*7594170eSAndroid Build Coastguard Worker    if add_message:
312*7594170eSAndroid Build Coastguard Worker      with open(f'{product_dashboard_folder}/product/build.log', 'a') as f:
313*7594170eSAndroid Build Coastguard Worker        f.write(f'\nPaths involving out/rbc are actually under {dirs.out_product}\n')
314*7594170eSAndroid Build Coastguard Worker
315*7594170eSAndroid Build Coastguard Worker  files = ['rbc_variable_dump.txt']
316*7594170eSAndroid Build Coastguard Worker  product_files = [(os.path.join(dirs.out_baseline, x), os.path.join(dirs.out_product, x)) for x in files]
317*7594170eSAndroid Build Coastguard Worker  product_has_diffs = await has_diffs(baseline_success and product_success, product_files, product_dashboard_folder+'/product')
318*7594170eSAndroid Build Coastguard Worker
319*7594170eSAndroid Build Coastguard Worker  cleanup_empty_files(product_dashboard_folder)
320*7594170eSAndroid Build Coastguard Worker
321*7594170eSAndroid Build Coastguard Worker  return ProductResult(product, baseline_success, product_success, product_has_diffs)
322*7594170eSAndroid Build Coastguard Worker
323*7594170eSAndroid Build Coastguard Worker
324*7594170eSAndroid Build Coastguard Workerasync def main():
325*7594170eSAndroid Build Coastguard Worker  parser = argparse.ArgumentParser(
326*7594170eSAndroid Build Coastguard Worker      description='Generates a dashboard of the starlark product configuration conversion.')
327*7594170eSAndroid Build Coastguard Worker  parser.add_argument('products', nargs='*',
328*7594170eSAndroid Build Coastguard Worker                      help='list of products to test. If not given, all '
329*7594170eSAndroid Build Coastguard Worker                      + 'products will be tested. '
330*7594170eSAndroid Build Coastguard Worker                      + 'Example: aosp_arm64-userdebug')
331*7594170eSAndroid Build Coastguard Worker  parser.add_argument('--quick', action='store_true',
332*7594170eSAndroid Build Coastguard Worker                      help='Run a quick test. This will only run config.mk and '
333*7594170eSAndroid Build Coastguard Worker                      + 'diff the make variables at the end of it, instead of '
334*7594170eSAndroid Build Coastguard Worker                      + 'diffing the full ninja files.')
335*7594170eSAndroid Build Coastguard Worker  parser.add_argument('--exclude', nargs='+', default=[],
336*7594170eSAndroid Build Coastguard Worker                      help='Exclude these producs from the build. Useful if not '
337*7594170eSAndroid Build Coastguard Worker                      + 'supplying a list of products manually.')
338*7594170eSAndroid Build Coastguard Worker  parser.add_argument('--results-directory',
339*7594170eSAndroid Build Coastguard Worker                      help='Directory to store results in. Defaults to $(OUT_DIR)/rbc_dashboard. '
340*7594170eSAndroid Build Coastguard Worker                      + 'Warning: will be cleared!')
341*7594170eSAndroid Build Coastguard Worker  parser.add_argument('--failure-message',
342*7594170eSAndroid Build Coastguard Worker                      help='Additional message to append to stderr on failure.')
343*7594170eSAndroid Build Coastguard Worker  args = parser.parse_args()
344*7594170eSAndroid Build Coastguard Worker
345*7594170eSAndroid Build Coastguard Worker  if args.results_directory:
346*7594170eSAndroid Build Coastguard Worker    args.results_directory = os.path.abspath(args.results_directory)
347*7594170eSAndroid Build Coastguard Worker
348*7594170eSAndroid Build Coastguard Worker  os.chdir(get_top())
349*7594170eSAndroid Build Coastguard Worker
350*7594170eSAndroid Build Coastguard Worker  def str_to_product(p: str) -> Product:
351*7594170eSAndroid Build Coastguard Worker    match = _PRODUCT_REGEX.fullmatch(p)
352*7594170eSAndroid Build Coastguard Worker    if not match:
353*7594170eSAndroid Build Coastguard Worker      sys.exit(f'Invalid product name: {p}. Example: aosp_arm64-trunk_staging-userdebug')
354*7594170eSAndroid Build Coastguard Worker    return Product(
355*7594170eSAndroid Build Coastguard Worker        match.group(1),
356*7594170eSAndroid Build Coastguard Worker        match.group(2) if match.group(2) else 'trunk_staging',
357*7594170eSAndroid Build Coastguard Worker        match.group(3) if match.group(3) else 'userdebug',
358*7594170eSAndroid Build Coastguard Worker    )
359*7594170eSAndroid Build Coastguard Worker
360*7594170eSAndroid Build Coastguard Worker  products = [str_to_product(p) for p in args.products]
361*7594170eSAndroid Build Coastguard Worker
362*7594170eSAndroid Build Coastguard Worker  if not products:
363*7594170eSAndroid Build Coastguard Worker    products = list(map(lambda x: Product(x, 'trunk_staging', 'userdebug'), get_build_var(
364*7594170eSAndroid Build Coastguard Worker        'all_named_products', Product('aosp_arm64', 'trunk_staging', 'userdebug')).split()))
365*7594170eSAndroid Build Coastguard Worker
366*7594170eSAndroid Build Coastguard Worker  excluded = [str_to_product(p) for p in args.exclude]
367*7594170eSAndroid Build Coastguard Worker  products = [p for p in products if p not in excluded]
368*7594170eSAndroid Build Coastguard Worker
369*7594170eSAndroid Build Coastguard Worker  for i, product in enumerate(products):
370*7594170eSAndroid Build Coastguard Worker    for j, product2 in enumerate(products):
371*7594170eSAndroid Build Coastguard Worker      if i != j and product.product == product2.product:
372*7594170eSAndroid Build Coastguard Worker        sys.exit(f'Product {product.product} cannot be repeated.')
373*7594170eSAndroid Build Coastguard Worker
374*7594170eSAndroid Build Coastguard Worker  out_dir = get_build_var('OUT_DIR', Product('aosp_arm64', 'trunk_staging', 'userdebug'))
375*7594170eSAndroid Build Coastguard Worker
376*7594170eSAndroid Build Coastguard Worker  dirs = Directories(
377*7594170eSAndroid Build Coastguard Worker      out=out_dir,
378*7594170eSAndroid Build Coastguard Worker      out_baseline=os.path.join(out_dir, 'rbc_out_baseline'),
379*7594170eSAndroid Build Coastguard Worker      out_product=os.path.join(out_dir, 'rbc_out_product'),
380*7594170eSAndroid Build Coastguard Worker      results=args.results_directory if args.results_directory else os.path.join(out_dir, 'rbc_dashboard'))
381*7594170eSAndroid Build Coastguard Worker
382*7594170eSAndroid Build Coastguard Worker  for folder in [dirs.out_baseline, dirs.out_product, dirs.results]:
383*7594170eSAndroid Build Coastguard Worker    # delete and recreate the out directories. You can't reuse them for
384*7594170eSAndroid Build Coastguard Worker    # a particular product, because after we delete some product-specific
385*7594170eSAndroid Build Coastguard Worker    # files inside the out dir to save space, the build will fail if you
386*7594170eSAndroid Build Coastguard Worker    # try to build the same product again.
387*7594170eSAndroid Build Coastguard Worker    shutil.rmtree(folder, ignore_errors=True)
388*7594170eSAndroid Build Coastguard Worker    os.makedirs(folder)
389*7594170eSAndroid Build Coastguard Worker
390*7594170eSAndroid Build Coastguard Worker  # When running in quick mode, we still need to build
391*7594170eSAndroid Build Coastguard Worker  # mk2rbc/rbcrun/AndroidProducts.mk.list, so run a get_build_var command to do
392*7594170eSAndroid Build Coastguard Worker  # that in each folder.
393*7594170eSAndroid Build Coastguard Worker  if args.quick:
394*7594170eSAndroid Build Coastguard Worker    commands = []
395*7594170eSAndroid Build Coastguard Worker    folders = [dirs.out_baseline, dirs.out_product]
396*7594170eSAndroid Build Coastguard Worker    for folder in folders:
397*7594170eSAndroid Build Coastguard Worker      commands.append(run_jailed_command([
398*7594170eSAndroid Build Coastguard Worker          'build/soong/soong_ui.bash',
399*7594170eSAndroid Build Coastguard Worker          '--dumpvar-mode',
400*7594170eSAndroid Build Coastguard Worker          'TARGET_PRODUCT',
401*7594170eSAndroid Build Coastguard Worker      ], folder, env = {
402*7594170eSAndroid Build Coastguard Worker          **os.environ,
403*7594170eSAndroid Build Coastguard Worker          'TARGET_PRODUCT': 'aosp_arm64',
404*7594170eSAndroid Build Coastguard Worker          'TARGET_RELEASE': 'trunk_staging',
405*7594170eSAndroid Build Coastguard Worker          'TARGET_BUILD_VARIANT': 'userdebug',
406*7594170eSAndroid Build Coastguard Worker      }))
407*7594170eSAndroid Build Coastguard Worker    for i, success in enumerate(await asyncio.gather(*commands)):
408*7594170eSAndroid Build Coastguard Worker      if not success:
409*7594170eSAndroid Build Coastguard Worker        dump_files_to_stderr(os.path.join(folders[i], 'build.log'))
410*7594170eSAndroid Build Coastguard Worker        sys.exit('Failed to setup output directories')
411*7594170eSAndroid Build Coastguard Worker
412*7594170eSAndroid Build Coastguard Worker  with open(os.path.join(dirs.results, 'index.html'), 'w') as f:
413*7594170eSAndroid Build Coastguard Worker    f.write(f'''
414*7594170eSAndroid Build Coastguard Worker      <body>
415*7594170eSAndroid Build Coastguard Worker        <h2>RBC Product/Board conversion status</h2>
416*7594170eSAndroid Build Coastguard Worker        Generated on {datetime.date.today()} for branch {get_branch()}
417*7594170eSAndroid Build Coastguard Worker        <table>
418*7594170eSAndroid Build Coastguard Worker          <tr>
419*7594170eSAndroid Build Coastguard Worker            <th>#</th>
420*7594170eSAndroid Build Coastguard Worker            <th>product</th>
421*7594170eSAndroid Build Coastguard Worker            <th>baseline</th>
422*7594170eSAndroid Build Coastguard Worker            <th>RBC product config</th>
423*7594170eSAndroid Build Coastguard Worker          </tr>\n''')
424*7594170eSAndroid Build Coastguard Worker    f.flush()
425*7594170eSAndroid Build Coastguard Worker
426*7594170eSAndroid Build Coastguard Worker    all_results = []
427*7594170eSAndroid Build Coastguard Worker    start_time = time.time()
428*7594170eSAndroid Build Coastguard Worker    print(f'{"Current product":31.31} | {"Time Elapsed":>16} | {"Per each":>8} | {"ETA":>16} | Status')
429*7594170eSAndroid Build Coastguard Worker    print('-' * 91)
430*7594170eSAndroid Build Coastguard Worker    for i, product in enumerate(products):
431*7594170eSAndroid Build Coastguard Worker      if i > 0:
432*7594170eSAndroid Build Coastguard Worker        elapsed_time = time.time() - start_time
433*7594170eSAndroid Build Coastguard Worker        time_per_product = elapsed_time / i
434*7594170eSAndroid Build Coastguard Worker        eta = time_per_product * (len(products) - i)
435*7594170eSAndroid Build Coastguard Worker        elapsed_time_str = str(datetime.timedelta(seconds=int(elapsed_time)))
436*7594170eSAndroid Build Coastguard Worker        time_per_product_str = str(datetime.timedelta(seconds=int(time_per_product)))
437*7594170eSAndroid Build Coastguard Worker        eta_str = str(datetime.timedelta(seconds=int(eta)))
438*7594170eSAndroid Build Coastguard Worker        print(f'{f"{i+1}/{len(products)} {product}":31.31} | {elapsed_time_str:>16} | {time_per_product_str:>8} | {eta_str:>16} | ', end='', flush=True)
439*7594170eSAndroid Build Coastguard Worker      else:
440*7594170eSAndroid Build Coastguard Worker        print(f'{f"{i+1}/{len(products)} {product}":31.31} | {"":>16} | {"":>8} | {"":>16} | ', end='', flush=True)
441*7594170eSAndroid Build Coastguard Worker
442*7594170eSAndroid Build Coastguard Worker      if not args.quick:
443*7594170eSAndroid Build Coastguard Worker        result = await test_one_product(product, dirs)
444*7594170eSAndroid Build Coastguard Worker      else:
445*7594170eSAndroid Build Coastguard Worker        result = await test_one_product_quick(product, dirs)
446*7594170eSAndroid Build Coastguard Worker
447*7594170eSAndroid Build Coastguard Worker      all_results.append(result)
448*7594170eSAndroid Build Coastguard Worker
449*7594170eSAndroid Build Coastguard Worker      if result.success():
450*7594170eSAndroid Build Coastguard Worker        print('Success')
451*7594170eSAndroid Build Coastguard Worker      else:
452*7594170eSAndroid Build Coastguard Worker        print('Failure')
453*7594170eSAndroid Build Coastguard Worker
454*7594170eSAndroid Build Coastguard Worker      f.write(generate_html_row(i+1, result))
455*7594170eSAndroid Build Coastguard Worker      f.flush()
456*7594170eSAndroid Build Coastguard Worker
457*7594170eSAndroid Build Coastguard Worker    baseline_successes = len([x for x in all_results if x.baseline_success])
458*7594170eSAndroid Build Coastguard Worker    product_successes = len([x for x in all_results if x.product_success and not x.product_has_diffs])
459*7594170eSAndroid Build Coastguard Worker    f.write(f'''
460*7594170eSAndroid Build Coastguard Worker          <tr>
461*7594170eSAndroid Build Coastguard Worker            <td></td>
462*7594170eSAndroid Build Coastguard Worker            <td># Successful</td>
463*7594170eSAndroid Build Coastguard Worker            <td>{baseline_successes}</td>
464*7594170eSAndroid Build Coastguard Worker            <td>{product_successes}</td>
465*7594170eSAndroid Build Coastguard Worker          </tr>
466*7594170eSAndroid Build Coastguard Worker          <tr>
467*7594170eSAndroid Build Coastguard Worker            <td></td>
468*7594170eSAndroid Build Coastguard Worker            <td># Failed</td>
469*7594170eSAndroid Build Coastguard Worker            <td>N/A</td>
470*7594170eSAndroid Build Coastguard Worker            <td>{baseline_successes - product_successes}</td>
471*7594170eSAndroid Build Coastguard Worker          </tr>
472*7594170eSAndroid Build Coastguard Worker        </table>
473*7594170eSAndroid Build Coastguard Worker        Finished running successfully.
474*7594170eSAndroid Build Coastguard Worker      </body>\n''')
475*7594170eSAndroid Build Coastguard Worker
476*7594170eSAndroid Build Coastguard Worker  print('Success!')
477*7594170eSAndroid Build Coastguard Worker  print('file://'+os.path.abspath(os.path.join(dirs.results, 'index.html')))
478*7594170eSAndroid Build Coastguard Worker
479*7594170eSAndroid Build Coastguard Worker  for result in all_results:
480*7594170eSAndroid Build Coastguard Worker    if not result.success():
481*7594170eSAndroid Build Coastguard Worker      print('There were one or more failing products. First failure:', file=sys.stderr)
482*7594170eSAndroid Build Coastguard Worker      dump_files_to_stderr(os.path.join(dirs.results, str(result.product)))
483*7594170eSAndroid Build Coastguard Worker      if args.failure_message:
484*7594170eSAndroid Build Coastguard Worker        print(args.failure_message, file=sys.stderr)
485*7594170eSAndroid Build Coastguard Worker      sys.exit(1)
486*7594170eSAndroid Build Coastguard Worker
487*7594170eSAndroid Build Coastguard Worker  baseline_failures = []
488*7594170eSAndroid Build Coastguard Worker  for result in all_results:
489*7594170eSAndroid Build Coastguard Worker    if result.product.product not in _ALREADY_FAILING_PRODUCTS and not result.baseline_success:
490*7594170eSAndroid Build Coastguard Worker      baseline_failures.append(result)
491*7594170eSAndroid Build Coastguard Worker  if baseline_failures:
492*7594170eSAndroid Build Coastguard Worker    product_str = "\n  ".join([f"{x.product}" for x in baseline_failures])
493*7594170eSAndroid Build Coastguard Worker    print(f"These products fail to run (Make-based) product config:\n  {product_str}\nFirst failure:", file=sys.stderr)
494*7594170eSAndroid Build Coastguard Worker    result = baseline_failures[0]
495*7594170eSAndroid Build Coastguard Worker    dump_files_to_stderr(os.path.join(dirs.results, str(result.product), 'baseline'))
496*7594170eSAndroid Build Coastguard Worker    if args.failure_message:
497*7594170eSAndroid Build Coastguard Worker      print(args.failure_message, file=sys.stderr)
498*7594170eSAndroid Build Coastguard Worker    sys.exit(1)
499*7594170eSAndroid Build Coastguard Worker
500*7594170eSAndroid Build Coastguard Worker
501*7594170eSAndroid Build Coastguard Workerif __name__ == '__main__':
502*7594170eSAndroid Build Coastguard Worker  asyncio.run(main())
503