xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/manifest_utils.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1*760c253cSXin Li# Copyright 2023 The ChromiumOS Authors
2*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be
3*760c253cSXin Li# found in the LICENSE file.
4*760c253cSXin Li
5*760c253cSXin Li"""Provides utilities to read and edit the ChromiumOS Manifest entries.
6*760c253cSXin Li
7*760c253cSXin LiWhile this code reads and edits the internal manifest, it should only operate
8*760c253cSXin Lion toolchain projects (llvm-project, etc.) which are public.
9*760c253cSXin Li"""
10*760c253cSXin Li
11*760c253cSXin Lifrom pathlib import Path
12*760c253cSXin Liimport shutil
13*760c253cSXin Liimport subprocess
14*760c253cSXin Lifrom typing import List, Optional, Union
15*760c253cSXin Lifrom xml.etree import ElementTree
16*760c253cSXin Li
17*760c253cSXin Liimport atomic_write_file
18*760c253cSXin Li
19*760c253cSXin Li
20*760c253cSXin LiLLVM_PROJECT_PATH = "src/third_party/llvm-project"
21*760c253cSXin Li
22*760c253cSXin Li
23*760c253cSXin Liclass FormattingError(Exception):
24*760c253cSXin Li    """Error occurred when formatting the manifest."""
25*760c253cSXin Li
26*760c253cSXin Li
27*760c253cSXin Liclass UpdateManifestError(Exception):
28*760c253cSXin Li    """Error occurred when updating the manifest."""
29*760c253cSXin Li
30*760c253cSXin Li
31*760c253cSXin Liclass ManifestParseError(Exception):
32*760c253cSXin Li    """Error occurred when parsing the contents of the manifest."""
33*760c253cSXin Li
34*760c253cSXin Li
35*760c253cSXin Lidef make_xmlparser() -> ElementTree.XMLParser:
36*760c253cSXin Li    """Return a new xmlparser with custom TreeBuilder."""
37*760c253cSXin Li    return ElementTree.XMLParser(
38*760c253cSXin Li        target=ElementTree.TreeBuilder(insert_comments=True)
39*760c253cSXin Li    )
40*760c253cSXin Li
41*760c253cSXin Li
42*760c253cSXin Lidef _find_llvm_project_in_manifest_tree(
43*760c253cSXin Li    xmlroot: ElementTree.Element,
44*760c253cSXin Li) -> Optional[ElementTree.Element]:
45*760c253cSXin Li    """Returns the llvm-project `project` in `xmlroot`, if it exists."""
46*760c253cSXin Li    for child in xmlroot:
47*760c253cSXin Li        if (
48*760c253cSXin Li            child.tag == "project"
49*760c253cSXin Li            and child.attrib.get("path") == LLVM_PROJECT_PATH
50*760c253cSXin Li        ):
51*760c253cSXin Li            return child
52*760c253cSXin Li    return None
53*760c253cSXin Li
54*760c253cSXin Li
55*760c253cSXin Lidef extract_current_llvm_hash(src_tree: Path) -> str:
56*760c253cSXin Li    """Returns the current LLVM SHA for the CrOS tree rooted at `src_tree`.
57*760c253cSXin Li
58*760c253cSXin Li    Raises:
59*760c253cSXin Li        ManifestParseError if the manifest didn't have the expected contents.
60*760c253cSXin Li    """
61*760c253cSXin Li    xmlroot = ElementTree.parse(
62*760c253cSXin Li        get_chromeos_manifest_path(src_tree), parser=make_xmlparser()
63*760c253cSXin Li    ).getroot()
64*760c253cSXin Li    return extract_current_llvm_hash_from_xml(xmlroot)
65*760c253cSXin Li
66*760c253cSXin Li
67*760c253cSXin Lidef extract_current_llvm_hash_from_xml(xmlroot: ElementTree.Element) -> str:
68*760c253cSXin Li    """Returns the current LLVM SHA for the parsed XML file.
69*760c253cSXin Li
70*760c253cSXin Li    Raises:
71*760c253cSXin Li        ManifestParseError if the manifest didn't have the expected contents.
72*760c253cSXin Li    """
73*760c253cSXin Li    if xmlroot.tag != "manifest":
74*760c253cSXin Li        raise ManifestParseError(
75*760c253cSXin Li            f"Root tag is {xmlroot.tag}; should be `manifest`."
76*760c253cSXin Li        )
77*760c253cSXin Li
78*760c253cSXin Li    llvm_project = _find_llvm_project_in_manifest_tree(xmlroot)
79*760c253cSXin Li    if llvm_project is None:
80*760c253cSXin Li        raise ManifestParseError("No llvm-project `project` found in manifest.")
81*760c253cSXin Li
82*760c253cSXin Li    revision = llvm_project.attrib.get("revision")
83*760c253cSXin Li    if not revision:
84*760c253cSXin Li        raise ManifestParseError("Toolchain's `project` has no revision.")
85*760c253cSXin Li
86*760c253cSXin Li    return revision
87*760c253cSXin Li
88*760c253cSXin Li
89*760c253cSXin Lidef update_chromeos_manifest(revision: str, src_tree: Path) -> Path:
90*760c253cSXin Li    """Replaces the manifest project revision with 'revision'.
91*760c253cSXin Li
92*760c253cSXin Li    Notably, this function reformats the manifest file to preserve
93*760c253cSXin Li    the formatting as specified by 'cros format'.
94*760c253cSXin Li
95*760c253cSXin Li    Args:
96*760c253cSXin Li        revision: Revision (git sha) to use in the manifest.
97*760c253cSXin Li        src_tree: Path to the root of the source tree checkout.
98*760c253cSXin Li
99*760c253cSXin Li    Returns:
100*760c253cSXin Li        The manifest path.
101*760c253cSXin Li
102*760c253cSXin Li    Post:
103*760c253cSXin Li        The llvm-project revision info in the chromeos repo manifest
104*760c253cSXin Li        is updated with 'revision'.
105*760c253cSXin Li
106*760c253cSXin Li    Raises:
107*760c253cSXin Li        UpdateManifestError: The manifest could not be changed.
108*760c253cSXin Li        FormattingError: The manifest could not be reformatted.
109*760c253cSXin Li    """
110*760c253cSXin Li    manifest_path = get_chromeos_manifest_path(src_tree)
111*760c253cSXin Li    parser = make_xmlparser()
112*760c253cSXin Li    xmltree = ElementTree.parse(manifest_path, parser)
113*760c253cSXin Li    update_chromeos_manifest_tree(revision, xmltree.getroot())
114*760c253cSXin Li    with atomic_write_file.atomic_write(manifest_path, mode="wb") as f:
115*760c253cSXin Li        xmltree.write(f, encoding="utf-8")
116*760c253cSXin Li    format_manifest(manifest_path)
117*760c253cSXin Li    return manifest_path
118*760c253cSXin Li
119*760c253cSXin Li
120*760c253cSXin Lidef get_chromeos_manifest_path(src_tree: Path) -> Path:
121*760c253cSXin Li    """Return the path to the toolchain manifest."""
122*760c253cSXin Li    return src_tree / "manifest-internal" / "_toolchain.xml"
123*760c253cSXin Li
124*760c253cSXin Li
125*760c253cSXin Lidef update_chromeos_manifest_tree(revision: str, xmlroot: ElementTree.Element):
126*760c253cSXin Li    """Update the revision info for LLVM for a manifest XML root."""
127*760c253cSXin Li    llvm_project_elem = _find_llvm_project_in_manifest_tree(xmlroot)
128*760c253cSXin Li    # Element objects can be falsy, so we need to explicitly check None.
129*760c253cSXin Li    if llvm_project_elem is None:
130*760c253cSXin Li        raise UpdateManifestError("xmltree did not have llvm-project")
131*760c253cSXin Li    llvm_project_elem.attrib["revision"] = revision
132*760c253cSXin Li
133*760c253cSXin Li
134*760c253cSXin Lidef format_manifest(repo_manifest: Path):
135*760c253cSXin Li    """Use cros format to format the given manifest."""
136*760c253cSXin Li    if not shutil.which("cros"):
137*760c253cSXin Li        raise FormattingError(
138*760c253cSXin Li            "unable to format manifest, 'cros'" " executable not in PATH"
139*760c253cSXin Li        )
140*760c253cSXin Li    cmd: List[Union[str, Path]] = ["cros", "format", repo_manifest]
141*760c253cSXin Li    subprocess.run(cmd, check=True)
142