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