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