xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/modify_a_tryjob.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"""Modifies a tryjob based off of arguments."""
7
8import argparse
9import enum
10import json
11import os
12from pathlib import Path
13import sys
14from typing import Dict, Iterable, List, Union
15
16import chroot
17import failure_modes
18import get_llvm_hash
19import git
20import update_chromeos_llvm_hash
21import update_packages_and_run_tests
22import update_tryjob_status
23
24
25class ModifyTryjob(enum.Enum):
26    """Options to modify a tryjob."""
27
28    REMOVE = "remove"
29    RELAUNCH = "relaunch"
30    ADD = "add"
31
32
33def GetCommandLineArgs() -> argparse.Namespace:
34    """Parses the command line for the command line arguments."""
35
36    # Default path to the chroot if a path is not specified.
37    cros_root = os.path.expanduser("~")
38    cros_root = os.path.join(cros_root, "chromiumos")
39
40    # Create parser and add optional command-line arguments.
41    parser = argparse.ArgumentParser(
42        description="Removes, relaunches, or adds a tryjob."
43    )
44
45    # Add argument for the JSON file to use for the update of a tryjob.
46    parser.add_argument(
47        "--status_file",
48        required=True,
49        help="The absolute path to the JSON file that contains the tryjobs "
50        "used for bisecting LLVM.",
51    )
52
53    # Add argument that determines what action to take on the revision
54    # specified.
55    parser.add_argument(
56        "--modify_tryjob",
57        required=True,
58        choices=[modify_tryjob.value for modify_tryjob in ModifyTryjob],
59        help="What action to perform on the tryjob.",
60    )
61
62    # Add argument that determines which revision to search for in the list of
63    # tryjobs.
64    parser.add_argument(
65        "--revision",
66        required=True,
67        type=int,
68        help="The revision to either remove or relaunch.",
69    )
70
71    # Add argument for other change lists that want to run alongside the
72    # tryjob.
73    parser.add_argument(
74        "--extra_change_lists",
75        type=int,
76        nargs="+",
77        help="change lists that would like to be run alongside the change list "
78        "of updating the packages",
79    )
80
81    # Add argument for custom options for the tryjob.
82    parser.add_argument(
83        "--options",
84        required=False,
85        nargs="+",
86        help="options to use for the tryjob testing",
87    )
88
89    # Add argument for the builder to use for the tryjob.
90    parser.add_argument(
91        "--builder", help="builder to use for the tryjob testing"
92    )
93
94    # Add argument for a specific chroot path.
95    parser.add_argument(
96        "--chromeos_path",
97        default=cros_root,
98        help="the path to the chroot (default: %(default)s)",
99    )
100
101    args_output = parser.parse_args()
102
103    if not os.path.isfile(
104        args_output.status_file
105    ) or not args_output.status_file.endswith(".json"):
106        raise ValueError(
107            'File does not exist or does not ending in ".json" '
108            ": %s" % args_output.status_file
109        )
110
111    if (
112        args_output.modify_tryjob == ModifyTryjob.ADD.value
113        and not args_output.builder
114    ):
115        raise ValueError("A builder is required for adding a tryjob.")
116    elif (
117        args_output.modify_tryjob != ModifyTryjob.ADD.value
118        and args_output.builder
119    ):
120        raise ValueError(
121            "Specifying a builder is only available when adding a " "tryjob."
122        )
123
124    return args_output
125
126
127def GetCLAfterUpdatingPackages(
128    packages: Iterable[str],
129    git_hash: str,
130    svn_version: int,
131    chromeos_path: Union[Path, str],
132    svn_option: Union[int, str],
133) -> git.CommitContents:
134    """Updates the packages' LLVM_NEXT."""
135
136    change_list = update_chromeos_llvm_hash.UpdatePackages(
137        packages=packages,
138        manifest_packages=[],
139        llvm_variant=update_chromeos_llvm_hash.LLVMVariant.next,
140        git_hash=git_hash,
141        svn_version=svn_version,
142        chroot_opts=update_chromeos_llvm_hash.ChrootOpts(Path(chromeos_path)),
143        mode=failure_modes.FailureModes.DISABLE_PATCHES,
144        git_hash_source=svn_option,
145        extra_commit_msg_lines=None,
146    )
147
148    # We are calling UpdatePackages with upload_changes=True, in
149    # which case it should always return a git.CommitContents value.
150    assert change_list is not None
151    print("\nSuccessfully updated packages to %d" % svn_version)
152    print("Gerrit URL: %s" % change_list.url)
153    print("Change list number: %d" % change_list.cl_number)
154
155    return change_list
156
157
158def CreateNewTryjobEntryForBisection(
159    cl: int,
160    extra_cls: List[int],
161    options: List[str],
162    builder: str,
163    chromeos_path: Union[Path, str],
164    cl_url: str,
165    revision,
166) -> Dict:
167    """Submits a tryjob and adds additional information."""
168
169    # Get the tryjob results after submitting the tryjob.
170    # Format of 'tryjob_results':
171    # [
172    #   {
173    #     'link' : [TRYJOB_LINK],
174    #     'buildbucket_id' : [BUILDBUCKET_ID],
175    #     'extra_cls' : [EXTRA_CLS_LIST],
176    #     'options' : [EXTRA_OPTIONS_LIST],
177    #     'builder' : [BUILDER_AS_A_LIST]
178    #   }
179    # ]
180    tryjob_results = update_packages_and_run_tests.RunTryJobs(
181        cl, extra_cls, options, [builder], chromeos_path
182    )
183    print("\nTryjob:")
184    print(tryjob_results[0])
185
186    # Add necessary information about the tryjob.
187    tryjob_results[0]["url"] = cl_url
188    tryjob_results[0]["rev"] = revision
189    tryjob_results[0][
190        "status"
191    ] = update_tryjob_status.TryjobStatus.PENDING.value
192    tryjob_results[0]["cl"] = cl
193
194    return tryjob_results[0]
195
196
197def AddTryjob(
198    packages: Iterable[str],
199    git_hash: str,
200    revision: int,
201    chromeos_path: Union[Path, str],
202    extra_cls: List[int],
203    options: List[str],
204    builder: str,
205    svn_option: Union[int, str],
206):
207    """Submits a tryjob."""
208
209    change_list = GetCLAfterUpdatingPackages(
210        packages,
211        git_hash,
212        revision,
213        chromeos_path,
214        svn_option,
215    )
216
217    tryjob_dict = CreateNewTryjobEntryForBisection(
218        change_list.cl_number,
219        extra_cls,
220        options,
221        builder,
222        chromeos_path,
223        change_list.url,
224        revision,
225    )
226
227    return tryjob_dict
228
229
230def PerformTryjobModification(
231    revision: int,
232    modify_tryjob: ModifyTryjob,
233    status_file: Union[Path, str],
234    extra_cls: List[int],
235    options: List[str],
236    builder: str,
237    chromeos_path: Union[Path, str],
238) -> None:
239    """Removes, relaunches, or adds a tryjob.
240
241    Args:
242        revision: The revision associated with the tryjob.
243        modify_tryjob: What action to take on the tryjob.
244          Ex: ModifyTryjob.REMOVE, ModifyTryjob.RELAUNCH, ModifyTryjob.ADD
245        status_file: The .JSON file that contains the tryjobs.
246        extra_cls: Extra change lists to be run alongside tryjob
247        options: Extra options to pass into 'cros tryjob'.
248        builder: The builder to use for 'cros tryjob'.
249        chromeos_path: The absolute path to the chromeos checkout.
250    """
251
252    # Format of 'bisect_contents':
253    # {
254    #   'start': [START_REVISION_OF_BISECTION]
255    #   'end': [END_REVISION_OF_BISECTION]
256    #   'jobs' : [
257    #       {[TRYJOB_INFORMATION]},
258    #       {[TRYJOB_INFORMATION]},
259    #       ...,
260    #       {[TRYJOB_INFORMATION]}
261    #   ]
262    # }
263    with open(status_file, encoding="utf-8") as tryjobs:
264        bisect_contents = json.load(tryjobs)
265
266    if not bisect_contents["jobs"] and modify_tryjob != ModifyTryjob.ADD:
267        sys.exit("No tryjobs in %s" % status_file)
268
269    tryjob_index = update_tryjob_status.FindTryjobIndex(
270        revision, bisect_contents["jobs"]
271    )
272
273    # 'FindTryjobIndex()' returns None if the tryjob was not found.
274    if tryjob_index is None and modify_tryjob != ModifyTryjob.ADD:
275        raise ValueError(
276            "Unable to find tryjob for %d in %s" % (revision, status_file)
277        )
278
279    # Determine the action to take based off of 'modify_tryjob'.
280    if modify_tryjob == ModifyTryjob.REMOVE:
281        del bisect_contents["jobs"][tryjob_index]
282
283        print("Successfully deleted the tryjob of revision %d" % revision)
284    elif modify_tryjob == ModifyTryjob.RELAUNCH:
285        # Need to update the tryjob link and buildbucket ID.
286        tryjob_results = update_packages_and_run_tests.RunTryJobs(
287            bisect_contents["jobs"][tryjob_index]["cl"],
288            bisect_contents["jobs"][tryjob_index]["extra_cls"],
289            bisect_contents["jobs"][tryjob_index]["options"],
290            bisect_contents["jobs"][tryjob_index]["builder"],
291            chromeos_path,
292        )
293
294        bisect_contents["jobs"][tryjob_index][
295            "status"
296        ] = update_tryjob_status.TryjobStatus.PENDING.value
297        bisect_contents["jobs"][tryjob_index]["link"] = tryjob_results[0][
298            "link"
299        ]
300        bisect_contents["jobs"][tryjob_index][
301            "buildbucket_id"
302        ] = tryjob_results[0]["buildbucket_id"]
303
304        print(
305            "Successfully relaunched the tryjob for revision %d and updated "
306            "the tryjob link to %s" % (revision, tryjob_results[0]["link"])
307        )
308    elif modify_tryjob == ModifyTryjob.ADD:
309        # Tryjob exists already.
310        if tryjob_index is not None:
311            raise ValueError(
312                "Tryjob already exists (index is %d) in %s."
313                % (tryjob_index, status_file)
314            )
315
316        # Make sure the revision is within the bounds of the start and end of
317        # the bisection.
318        elif bisect_contents["start"] < revision < bisect_contents["end"]:
319            (
320                git_hash,
321                revision,
322            ) = get_llvm_hash.GetLLVMHashAndVersionFromSVNOption(revision)
323
324            tryjob_dict = AddTryjob(
325                update_chromeos_llvm_hash.DEFAULT_PACKAGES,
326                git_hash,
327                revision,
328                chromeos_path,
329                extra_cls,
330                options,
331                builder,
332                revision,
333            )
334
335            bisect_contents["jobs"].append(tryjob_dict)
336
337            print("Successfully added tryjob of revision %d" % revision)
338        else:
339            raise ValueError("Failed to add tryjob to %s" % status_file)
340    else:
341        raise ValueError(
342            'Invalid "modify_tryjob" option provided: %s' % modify_tryjob
343        )
344
345    with open(status_file, "w", encoding="utf-8") as update_tryjobs:
346        json.dump(
347            bisect_contents, update_tryjobs, indent=4, separators=(",", ": ")
348        )
349
350
351def main() -> None:
352    """Removes, relaunches, or adds a tryjob."""
353
354    chroot.VerifyOutsideChroot()
355
356    args_output = GetCommandLineArgs()
357
358    chroot.VerifyChromeOSRoot(args_output.chromeos_path)
359
360    PerformTryjobModification(
361        args_output.revision,
362        ModifyTryjob(args_output.modify_tryjob),
363        args_output.status_file,
364        args_output.extra_change_lists,
365        args_output.options,
366        args_output.builder,
367        args_output.chromeos_path,
368    )
369
370
371if __name__ == "__main__":
372    main()
373