1*3c875a21SAndroid Build Coastguard Worker# 2*3c875a21SAndroid Build Coastguard Worker# Copyright (C) 2023 The Android Open Source Project 3*3c875a21SAndroid Build Coastguard Worker# 4*3c875a21SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the 'License'); 5*3c875a21SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 6*3c875a21SAndroid Build Coastguard Worker# You may obtain a copy of the License at 7*3c875a21SAndroid Build Coastguard Worker# 8*3c875a21SAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 9*3c875a21SAndroid Build Coastguard Worker# 10*3c875a21SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 11*3c875a21SAndroid Build Coastguard Worker# distributed under the License is distributed on an 'AS IS' BASIS, 12*3c875a21SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13*3c875a21SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 14*3c875a21SAndroid Build Coastguard Worker# limitations under the License. 15*3c875a21SAndroid Build Coastguard Worker# 16*3c875a21SAndroid Build Coastguard Worker"""Manifest discovery and parsing. 17*3c875a21SAndroid Build Coastguard Worker 18*3c875a21SAndroid Build Coastguard WorkerThe repo manifest format is documented at 19*3c875a21SAndroid Build Coastguard Workerhttps://gerrit.googlesource.com/git-repo/+/master/docs/manifest-format.md. This module 20*3c875a21SAndroid Build Coastguard Workerdoesn't implement the full spec, since we only need a few properties. 21*3c875a21SAndroid Build Coastguard Worker""" 22*3c875a21SAndroid Build Coastguard Workerfrom __future__ import annotations 23*3c875a21SAndroid Build Coastguard Worker 24*3c875a21SAndroid Build Coastguard Workerfrom dataclasses import dataclass 25*3c875a21SAndroid Build Coastguard Workerfrom pathlib import Path 26*3c875a21SAndroid Build Coastguard Workerfrom xml.etree import ElementTree 27*3c875a21SAndroid Build Coastguard Worker 28*3c875a21SAndroid Build Coastguard Worker 29*3c875a21SAndroid Build Coastguard Workerdef find_manifest_xml_for_tree(root: Path) -> Path: 30*3c875a21SAndroid Build Coastguard Worker """Returns the path to the manifest XML file for the tree.""" 31*3c875a21SAndroid Build Coastguard Worker repo_path = root / ".repo/manifests/default.xml" 32*3c875a21SAndroid Build Coastguard Worker if repo_path.exists(): 33*3c875a21SAndroid Build Coastguard Worker return repo_path 34*3c875a21SAndroid Build Coastguard Worker raise FileNotFoundError(f"Could not find manifest at {repo_path}") 35*3c875a21SAndroid Build Coastguard Worker 36*3c875a21SAndroid Build Coastguard Worker 37*3c875a21SAndroid Build Coastguard Worker@dataclass(frozen=True) 38*3c875a21SAndroid Build Coastguard Workerclass Project: 39*3c875a21SAndroid Build Coastguard Worker """Data for a manifest <project /> field. 40*3c875a21SAndroid Build Coastguard Worker 41*3c875a21SAndroid Build Coastguard Worker https://gerrit.googlesource.com/git-repo/+/master/docs/manifest-format.md#element-project 42*3c875a21SAndroid Build Coastguard Worker """ 43*3c875a21SAndroid Build Coastguard Worker 44*3c875a21SAndroid Build Coastguard Worker path: str 45*3c875a21SAndroid Build Coastguard Worker remote: str 46*3c875a21SAndroid Build Coastguard Worker revision: str 47*3c875a21SAndroid Build Coastguard Worker 48*3c875a21SAndroid Build Coastguard Worker @staticmethod 49*3c875a21SAndroid Build Coastguard Worker def from_xml_node( 50*3c875a21SAndroid Build Coastguard Worker node: ElementTree.Element, default_remote: str, default_revision: str 51*3c875a21SAndroid Build Coastguard Worker ) -> Project: 52*3c875a21SAndroid Build Coastguard Worker """Parses a Project from the given XML node.""" 53*3c875a21SAndroid Build Coastguard Worker try: 54*3c875a21SAndroid Build Coastguard Worker # Path is optional, defaults to project name per manifest spec 55*3c875a21SAndroid Build Coastguard Worker path = x if (x := node.attrib.get("path")) is not None else node.attrib["name"] 56*3c875a21SAndroid Build Coastguard Worker except KeyError as ex: 57*3c875a21SAndroid Build Coastguard Worker raise RuntimeError( 58*3c875a21SAndroid Build Coastguard Worker f"<project /> element missing required name attribute: {node}" 59*3c875a21SAndroid Build Coastguard Worker ) from ex 60*3c875a21SAndroid Build Coastguard Worker 61*3c875a21SAndroid Build Coastguard Worker return Project( 62*3c875a21SAndroid Build Coastguard Worker path, 63*3c875a21SAndroid Build Coastguard Worker node.attrib.get("remote", default_remote), 64*3c875a21SAndroid Build Coastguard Worker node.attrib.get("revision", default_revision), 65*3c875a21SAndroid Build Coastguard Worker ) 66*3c875a21SAndroid Build Coastguard Worker 67*3c875a21SAndroid Build Coastguard Worker 68*3c875a21SAndroid Build Coastguard Workerclass ManifestParser: # pylint: disable=too-few-public-methods 69*3c875a21SAndroid Build Coastguard Worker """Parser for the repo manifest.xml.""" 70*3c875a21SAndroid Build Coastguard Worker 71*3c875a21SAndroid Build Coastguard Worker def __init__(self, xml_path: Path) -> None: 72*3c875a21SAndroid Build Coastguard Worker self.xml_path = xml_path 73*3c875a21SAndroid Build Coastguard Worker 74*3c875a21SAndroid Build Coastguard Worker def parse(self) -> Manifest: 75*3c875a21SAndroid Build Coastguard Worker """Parses the manifest.xml file and returns a Manifest.""" 76*3c875a21SAndroid Build Coastguard Worker root = ElementTree.parse(self.xml_path) 77*3c875a21SAndroid Build Coastguard Worker defaults = root.findall("./default") 78*3c875a21SAndroid Build Coastguard Worker if len(defaults) != 1: 79*3c875a21SAndroid Build Coastguard Worker raise RuntimeError( 80*3c875a21SAndroid Build Coastguard Worker f"Expected exactly one <default /> element, found {len(defaults)}" 81*3c875a21SAndroid Build Coastguard Worker ) 82*3c875a21SAndroid Build Coastguard Worker default_node = defaults[0] 83*3c875a21SAndroid Build Coastguard Worker try: 84*3c875a21SAndroid Build Coastguard Worker default_revision = default_node.attrib["revision"] 85*3c875a21SAndroid Build Coastguard Worker default_remote = default_node.attrib["remote"] 86*3c875a21SAndroid Build Coastguard Worker except KeyError as ex: 87*3c875a21SAndroid Build Coastguard Worker raise RuntimeError("<default /> element missing required attribute") from ex 88*3c875a21SAndroid Build Coastguard Worker 89*3c875a21SAndroid Build Coastguard Worker return Manifest( 90*3c875a21SAndroid Build Coastguard Worker self.xml_path, 91*3c875a21SAndroid Build Coastguard Worker [ 92*3c875a21SAndroid Build Coastguard Worker Project.from_xml_node(p, default_remote, default_revision) 93*3c875a21SAndroid Build Coastguard Worker for p in root.findall("./project") 94*3c875a21SAndroid Build Coastguard Worker ], 95*3c875a21SAndroid Build Coastguard Worker ) 96*3c875a21SAndroid Build Coastguard Worker 97*3c875a21SAndroid Build Coastguard Worker 98*3c875a21SAndroid Build Coastguard Workerclass Manifest: 99*3c875a21SAndroid Build Coastguard Worker """The manifest data for a repo tree. 100*3c875a21SAndroid Build Coastguard Worker 101*3c875a21SAndroid Build Coastguard Worker https://gerrit.googlesource.com/git-repo/+/master/docs/manifest-format.md 102*3c875a21SAndroid Build Coastguard Worker """ 103*3c875a21SAndroid Build Coastguard Worker 104*3c875a21SAndroid Build Coastguard Worker def __init__(self, path: Path, projects: list[Project]) -> None: 105*3c875a21SAndroid Build Coastguard Worker self.path = path 106*3c875a21SAndroid Build Coastguard Worker self.projects_by_path = {p.path: p for p in projects} 107*3c875a21SAndroid Build Coastguard Worker 108*3c875a21SAndroid Build Coastguard Worker @staticmethod 109*3c875a21SAndroid Build Coastguard Worker def for_tree(root: Path) -> Manifest: 110*3c875a21SAndroid Build Coastguard Worker """Constructs a Manifest for the tree at `root`.""" 111*3c875a21SAndroid Build Coastguard Worker return ManifestParser(find_manifest_xml_for_tree(root)).parse() 112*3c875a21SAndroid Build Coastguard Worker 113*3c875a21SAndroid Build Coastguard Worker def project_with_path(self, path: str) -> Project: 114*3c875a21SAndroid Build Coastguard Worker """Returns the Project with the given path, or raises KeyError.""" 115*3c875a21SAndroid Build Coastguard Worker return self.projects_by_path[path] 116