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