xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/update_packages_and_run_tests.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1#!/usr/bin/env python3
2# Copyright 2019 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Runs a tryjob/tryjobs after updating the packages."""
7
8import argparse
9import datetime
10import json
11import os
12from pathlib import Path
13import subprocess
14from typing import Any, Dict, Iterable, List, Optional, Union
15
16import chroot
17import failure_modes
18import get_llvm_hash
19import update_chromeos_llvm_hash
20
21
22VALID_CQ_TRYBOTS = ("llvm", "llvm-next")
23
24
25def GetCommandLineArgs() -> argparse.Namespace:
26    """Parses the command line for the command line arguments.
27
28    Returns:
29        The log level to use when retrieving the LLVM hash or google3 LLVM
30        version, the chroot path to use for executing chroot commands,
31        a list of a package or packages to update their LLVM next hash,
32        and the LLVM version to use when retrieving the LLVM hash.
33    """
34
35    # Default path to the chroot if a path is not specified.
36    cros_root = os.path.expanduser("~")
37    cros_root = os.path.join(cros_root, "chromiumos")
38
39    # Create parser and add optional command-line arguments.
40    parser = argparse.ArgumentParser(
41        description="Update an LLVM hash of packages and run tests."
42    )
43
44    # Add argument for other change lists that want to run alongside the tryjob
45    # which has a change list of updating a package's git hash.
46    parser.add_argument(
47        "--extra_change_lists",
48        type=int,
49        nargs="+",
50        default=[],
51        help="change lists that would like to be run alongside the change list "
52        "of updating the packages",
53    )
54
55    # Add argument for a specific chroot path.
56    parser.add_argument(
57        "--chromeos_path",
58        default=cros_root,
59        help="the path to the ChromeOS tree (default: %(default)s)",
60    )
61
62    # Add argument for a specific chroot path.
63    parser.add_argument(
64        "--chroot_name",
65        default="chroot",
66        help="""
67        the name of the chroot to use in the CrOS checkout. Defaults to
68        'chroot'.
69        """,
70    )
71
72    parser.add_argument(
73        "--chroot_out",
74        help="""
75        the name of the chroot to use in the CrOS checkout. Defaults to
76        'out' if the chroot's name is 'chroot'; otherwise, defaults to
77        '${chroot_name}_out'.
78        """,
79    )
80
81    # Add argument to choose between llvm and llvm-next.
82    parser.add_argument(
83        "--is_llvm_next",
84        action="store_true",
85        help="which llvm hash to update. Update LLVM_NEXT_HASH if specified. "
86        "Otherwise, update LLVM_HASH",
87    )
88
89    # Add argument for the absolute path to the file that contains information
90    # on the previous tested svn version.
91    parser.add_argument(
92        "--last_tested",
93        help="the absolute path to the file that contains the last tested "
94        "arguments.",
95    )
96
97    # Add argument for the LLVM version to use.
98    parser.add_argument(
99        "--llvm_version",
100        type=get_llvm_hash.IsSvnOption,
101        required=True,
102        help="which git hash of LLVM to find "
103        "{google3, ToT, <svn_version>} "
104        "(default: finds the git hash of the google3 LLVM "
105        "version)",
106    )
107
108    # Add argument to add reviewers for the created CL.
109    parser.add_argument(
110        "--reviewers",
111        nargs="+",
112        default=[],
113        help="The reviewers for the package update changelist",
114    )
115
116    subparsers = parser.add_subparsers(dest="subparser_name")
117    subparser_names = []
118    # Testing with the tryjobs.
119    tryjob_subparser = subparsers.add_parser("tryjobs")
120    subparser_names.append("tryjobs")
121    tryjob_subparser.add_argument(
122        "--builders",
123        required=True,
124        nargs="+",
125        default=[],
126        help="builders to use for the tryjob testing",
127    )
128
129    # Add argument for custom options for the tryjob.
130    tryjob_subparser.add_argument(
131        "--options",
132        required=False,
133        nargs="+",
134        default=[],
135        help="options to use for the tryjob testing",
136    )
137
138    # Testing with the recipe builders
139    recipe_subparser = subparsers.add_parser("recipe")
140    subparser_names.append("recipe")
141    recipe_subparser.add_argument(
142        "--options",
143        required=False,
144        nargs="+",
145        default=[],
146        help="options passed to the recipe builders",
147    )
148
149    recipe_subparser.add_argument(
150        "--builders",
151        required=True,
152        nargs="+",
153        default=[],
154        help="recipe builders to launch",
155    )
156
157    # Testing with CQ.
158    cq_subparser = subparsers.add_parser("cq")
159    subparser_names.append("cq")
160
161    # Add argument for specify a cq trybot to test along with other cq builders
162    # e.g. llvm, llvm-next or llvm-tot
163    cq_subparser.add_argument(
164        "--cq_trybot",
165        choices=VALID_CQ_TRYBOTS,
166        help="include the trybot to test together with other cq builders "
167        "available: %(choices)s",
168    )
169
170    args_output = parser.parse_args()
171
172    if args_output.subparser_name not in subparser_names:
173        parser.error("one of %s must be specified" % subparser_names)
174
175    if not args_output.chroot_out:
176        chroot_name = args_output.chroot_name
177        if chroot_name == "chroot":
178            out = "out"
179        else:
180            out = f"{chroot_name}_out"
181        args_output.chroot_out = out
182
183    return args_output
184
185
186def UnchangedSinceLastRun(
187    last_tested_file: Optional[Union[Path, str]],
188    arg_dict: Dict,
189) -> bool:
190    """Gets the arguments used for last run
191
192    Args:
193        last_tested_file: The absolute path to the file that contains the
194          arguments for the last run.
195        arg_dict: The arguments used for this run.
196
197    Returns:
198        Return true if the arguments used for last run exist and are the
199        same as the arguments used for this run. Otherwise return false.
200    """
201
202    if not last_tested_file:
203        return False
204
205    # Get the last tested svn version if the file exists.
206    last_arg_dict = None
207    try:
208        with open(last_tested_file, encoding="utf-8") as f:
209            last_arg_dict = json.load(f)
210
211    except (IOError, ValueError):
212        return False
213
214    return arg_dict == last_arg_dict
215
216
217def AddReviewers(
218    cl: int,
219    reviewers: Iterable[str],
220    chromeos_path: Union[Path, str],
221) -> None:
222    """Add reviewers for the created CL.
223
224    Args:
225        cl: The CL number to add reviewers to.
226        reviewers: Email addresses of reviewers to add.
227        chromeos_path: The absolute path to the chromeos tree.
228    """
229
230    gerrit_abs_path = os.path.join(chromeos_path, "chromite/bin/gerrit")
231    for reviewer in reviewers:
232        cmd = [gerrit_abs_path, "reviewers", str(cl), reviewer]
233
234        subprocess.check_output(cmd)
235
236
237def AddLinksToCL(
238    tests: Iterable[Dict[str, Any]],
239    cl: int,
240    chromeos_path: Union[Path, str],
241) -> None:
242    """Adds the test link(s) to the CL as a comment.
243
244    Args:
245        tests: Links to the tests.
246        cl: The number of the CL to add the test links to.
247        chromeos_path: Absolute path to the chromeos tree.
248    """
249
250    # NOTE: Invoking `cros_sdk` does not make each tryjob link appear on its
251    # own line, so invoking the `gerrit` command directly instead of using
252    # `cros_sdk` to do it for us.
253    #
254    # FIXME: Need to figure out why `cros_sdk` does not add each tryjob link as
255    # a newline.
256    gerrit_abs_path = os.path.join(chromeos_path, "chromite/bin/gerrit")
257
258    links = ["Started the following tests:"]
259    links.extend(test["link"] for test in tests)
260
261    add_message_cmd = [gerrit_abs_path, "message", str(cl), "\n".join(links)]
262
263    subprocess.check_output(add_message_cmd)
264
265
266# Testing with tryjobs
267def GetCurrentTimeInUTC() -> datetime.datetime:
268    """Returns the current time via `datetime.datetime.utcnow()`."""
269    return datetime.datetime.utcnow()
270
271
272def GetTryJobCommand(
273    change_list: int,
274    extra_change_lists: Iterable[int],
275    options: Iterable[str],
276    builder: str,
277) -> List[str]:
278    """Constructs the 'tryjob' command.
279
280    Args:
281        change_list: The CL obtained from updating the packages.
282        extra_change_lists: Extra change lists that would like to be run
283          alongside the change list of updating the packages.
284        options: Options to be passed into the tryjob command.
285        builder: The builder to be passed into the tryjob command.
286
287    Returns:
288        The 'tryjob' command with the change list of updating the packages and
289        any extra information that was passed into the command line.
290    """
291
292    tryjob_cmd = ["cros", "tryjob", "--yes", "--json", "-g", "%d" % change_list]
293
294    if extra_change_lists:
295        for extra_cl in extra_change_lists:
296            tryjob_cmd.extend(["-g", "%d" % extra_cl])
297
298    if options:
299        tryjob_cmd.extend("--%s" % option for option in options)
300
301    tryjob_cmd.append(builder)
302
303    return tryjob_cmd
304
305
306def RunTryJobs(
307    cl_number: int,
308    extra_change_lists: List[int],
309    options: List[str],
310    builders: Iterable[str],
311    chromeos_path: Union[Path, str],
312) -> List[Dict]:
313    """Runs a tryjob/tryjobs.
314
315    Args:
316        cl_number: The CL created by updating the packages.
317        extra_change_lists: Any extra change lists that would run alongside the
318          CL that was created by updating the packages ('cl_number').
319        options: Any options to be passed into the 'tryjob' command.
320        builders: All the builders to run the 'tryjob' with.
321        chromeos_path: The absolute path to the chromeos tree.
322
323    Returns:
324        A list that contains stdout contents of each tryjob, where stdout is
325        information (a hashmap) about the tryjob. The hashmap also contains
326        stderr if there was an error when running a tryjob.
327
328    Raises:
329        ValueError: Failed to submit a tryjob.
330    """
331
332    # Contains the results of each builder.
333    tests = []
334
335    # Run tryjobs with the change list number obtained from updating the
336    # packages and append additional changes lists and options obtained from the
337    # command line.
338    for builder in builders:
339        cmd = GetTryJobCommand(cl_number, extra_change_lists, options, builder)
340
341        out = subprocess.check_output(cmd, cwd=chromeos_path, encoding="utf-8")
342
343        test_output = json.loads(out)
344
345        buildbucket_id = int(test_output[0]["id"])
346
347        tests.append(
348            {
349                "launch_time": str(GetCurrentTimeInUTC()),
350                "link": "http://ci.chromium.org/b/%s" % buildbucket_id,
351                "buildbucket_id": buildbucket_id,
352                "extra_cls": extra_change_lists,
353                "options": options,
354                "builder": [builder],
355            }
356        )
357
358    AddLinksToCL(tests, cl_number, chromeos_path)
359
360    return tests
361
362
363def StartRecipeBuilders(
364    cl_number: int,
365    extra_change_lists: List[int],
366    options: List[str],
367    builders: List[str],
368    chromeos_path: Union[Path, str],
369) -> List[Dict]:
370    """Launch recipe builders.
371
372    Args:
373        cl_number: The CL created by updating the packages.
374        extra_change_lists: Any extra change lists that would run alongside the
375          CL that was created by updating the packages ('cl_number').
376        options: Any options to be passed into the 'tryjob' command.
377        builders: All the builders to run the 'tryjob' with.
378        chromeos_path: The absolute path to the chromeos tree.
379
380    Returns:
381        A list that contains stdout contents of each builder, where stdout is
382        information (a hashmap) about the tryjob. The hashmap also contains
383        stderr if there was an error when running a tryjob.
384
385    Raises:
386        ValueError: Failed to start a builder.
387    """
388
389    # Contains the results of each builder.
390    tests = []
391
392    # Launch a builders with the change list number obtained from updating the
393    # packages and append additional changes lists and options obtained from the
394    # command line.
395    for builder in builders:
396        cmd = ["bb", "add", "-json"]
397
398        if cl_number:
399            cmd.extend(["-cl", "crrev.com/c/%d" % cl_number])
400
401        if extra_change_lists:
402            for cl in extra_change_lists:
403                cmd.extend(["-cl", "crrev.com/c/%d" % cl])
404
405        if options:
406            cmd.extend(options)
407
408        cmd.append(builder)
409
410        out = subprocess.check_output(cmd, cwd=chromeos_path, encoding="utf-8")
411
412        test_output = json.loads(out)
413
414        tests.append(
415            {
416                "launch_time": test_output["createTime"],
417                "link": "http://ci.chromium.org/b/%s" % test_output["id"],
418                "buildbucket_id": test_output["id"],
419                "extra_cls": extra_change_lists,
420                "options": options,
421                "builder": [builder],
422            }
423        )
424
425    AddLinksToCL(tests, cl_number, chromeos_path)
426
427    return tests
428
429
430# Testing with CQ
431def GetCQDependString(dependent_cls: List[int]) -> Optional[str]:
432    """Get CQ dependency string e.g. `Cq-Depend: chromium:MM, chromium:NN`."""
433
434    if not dependent_cls:
435        return None
436
437    # Cq-Depend must start a new paragraph prefixed with "Cq-Depend".
438    return "Cq-Depend: " + ", ".join(f"chromium:{x}" for x in dependent_cls)
439
440
441def GetCQIncludeTrybotsString(trybot: Optional[str]) -> Optional[str]:
442    """Get Cq-Include-Trybots string, for more llvm testings"""
443
444    if not trybot:
445        return None
446
447    if trybot not in VALID_CQ_TRYBOTS:
448        raise ValueError("%s is not a valid llvm trybot" % trybot)
449
450    # Cq-Include-Trybots must start a new paragraph prefixed
451    # with "Cq-Include-Trybots".
452    return "Cq-Include-Trybots:chromeos/cq:cq-%s-orchestrator" % trybot
453
454
455def StartCQDryRun(
456    cl: int,
457    dependent_cls: List[int],
458    chromeos_path: Union[Path, str],
459) -> None:
460    """Start CQ dry run for the changelist and dependencies."""
461
462    gerrit_abs_path = os.path.join(chromeos_path, "chromite/bin/gerrit")
463
464    cl_list = [cl]
465    cl_list.extend(dependent_cls)
466
467    for changes in cl_list:
468        cq_dry_run_cmd = [gerrit_abs_path, "label-cq", str(changes), "1"]
469
470        subprocess.check_output(cq_dry_run_cmd)
471
472
473def main():
474    """Updates the packages' LLVM hash and run tests.
475
476    Raises:
477        AssertionError: The script was run inside the chroot.
478    """
479
480    chroot.VerifyOutsideChroot()
481
482    args_output = GetCommandLineArgs()
483
484    chroot.VerifyChromeOSRoot(args_output.chromeos_path)
485
486    svn_option = args_output.llvm_version
487
488    git_hash, svn_version = get_llvm_hash.GetLLVMHashAndVersionFromSVNOption(
489        svn_option
490    )
491
492    # There is no need to run tryjobs when all the key parameters remain
493    # unchanged from last time.
494
495    # If --last_tested is specified, check if the current run has the same
496    # arguments last time --last_tested is used.
497    if args_output.last_tested:
498        chroot_file_paths = chroot.GetChrootEbuildPaths(
499            args_output.chromeos_path,
500            update_chromeos_llvm_hash.DEFAULT_PACKAGES,
501            args_output.chroot_name,
502            args_output.chroot_out,
503        )
504        arg_dict = {
505            "svn_version": svn_version,
506            "ebuilds": chroot_file_paths,
507            "extra_cls": args_output.extra_change_lists,
508        }
509        if args_output.subparser_name in ("tryjobs", "recipe"):
510            arg_dict["builders"] = args_output.builders
511            arg_dict["tryjob_options"] = args_output.options
512        if UnchangedSinceLastRun(args_output.last_tested, arg_dict):
513            print(
514                "svn version (%d) matches the last tested svn version in %s"
515                % (svn_version, args_output.last_tested)
516            )
517            return
518
519    llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current
520    if args_output.is_llvm_next:
521        llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next
522
523    extra_commit_msg_lines = []
524    if args_output.subparser_name == "cq":
525        footers = []
526        cq_depend_msg = GetCQDependString(args_output.extra_change_lists)
527        if cq_depend_msg:
528            footers.append(cq_depend_msg)
529        cq_trybot_msg = GetCQIncludeTrybotsString(args_output.cq_trybot)
530        if cq_trybot_msg:
531            footers.append(cq_trybot_msg)
532
533        # We want a single blank line before any of these, so Git properly
534        # interprets them as a footer.
535        if footers:
536            extra_commit_msg_lines.append("")
537            extra_commit_msg_lines += footers
538
539    change_list = update_chromeos_llvm_hash.UpdatePackages(
540        packages=update_chromeos_llvm_hash.DEFAULT_PACKAGES,
541        manifest_packages=[],
542        llvm_variant=llvm_variant,
543        git_hash=git_hash,
544        svn_version=svn_version,
545        chroot_opts=update_chromeos_llvm_hash.ChrootOpts(
546            chromeos_root=Path(args_output.chromeos_path),
547            chroot_name=args_output.chroot_name,
548            out_name=args_output.chroot_out,
549        ),
550        mode=failure_modes.FailureModes.DISABLE_PATCHES,
551        git_hash_source=svn_option,
552        extra_commit_msg_lines=extra_commit_msg_lines,
553        # b/331607705: set WIP on these changes, so the code-review-nudger bot
554        # doesn't ping them.
555        wip=True,
556    )
557
558    AddReviewers(
559        change_list.cl_number, args_output.reviewers, args_output.chromeos_path
560    )
561
562    print("Successfully updated packages to %d" % svn_version)
563    print("Gerrit URL: %s" % change_list.url)
564    print("Change list number: %d" % change_list.cl_number)
565
566    if args_output.subparser_name == "tryjobs":
567        tests = RunTryJobs(
568            change_list.cl_number,
569            args_output.extra_change_lists,
570            args_output.options,
571            args_output.builders,
572            args_output.chromeos_path,
573        )
574        print("Tests:")
575        for test in tests:
576            print(test)
577    elif args_output.subparser_name == "recipe":
578        tests = StartRecipeBuilders(
579            change_list.cl_number,
580            args_output.extra_change_lists,
581            args_output.options,
582            args_output.builders,
583            args_output.chromeos_path,
584        )
585        print("Tests:")
586        for test in tests:
587            print(test)
588
589    else:
590        StartCQDryRun(
591            change_list.cl_number,
592            args_output.extra_change_lists,
593            args_output.chromeos_path,
594        )
595
596    # If --last_tested is specified, record the arguments used
597    if args_output.last_tested:
598        with open(args_output.last_tested, "w", encoding="utf-8") as f:
599            json.dump(arg_dict, f, indent=2)
600
601
602if __name__ == "__main__":
603    main()
604