| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2023 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Utilities for working with GnLabel strings.""" |
| |
| import dataclasses |
| import os |
| from pathlib import Path |
| from typing import Any, List |
| |
| |
| @dataclasses.dataclass(frozen=True) |
| class GnLabel: |
| """Utility for handling Gn target labels""" |
| |
| """The original GN label string, e.g. `//foo/bar:baz(//toolchain)`""" |
| gn_str: str |
| """The path part of the label, e.g. `foo/bar` in `//foo/bar:baz`""" |
| # path: Path = dataclasses.field(hash=False, compare=False) |
| path_str: str = dataclasses.field(hash=False, compare=False) |
| """The name part of the label, e.g. `baz` in `//foo/bar:baz` or `bar` for `//foo/bar`""" |
| name: str = dataclasses.field(hash=False, compare=False) |
| """Whether the label has a local name, e.g. True for `//foo/bar:baz` but false for `//foo/bar/baz`""" |
| is_local_name: bool = dataclasses.field(hash=False, compare=False) |
| """The toolchain part of the label, e.g. `//toolchain` in `//foo/bar(//toolchain)`""" |
| toolchain: "GnLabel|None" = dataclasses.field(hash=False, compare=False) |
| |
| @staticmethod |
| def from_str(original_str: str) -> "GnLabel": |
| """Constructs a GnLabel instance from a GN target label string""" |
| assert original_str.startswith( |
| "//" |
| ), f"label must start with // but got {original_str}" |
| |
| assert not original_str.startswith( |
| "///" |
| ), f"label can't start with /// but got {original_str}" |
| |
| label = original_str |
| toolchain = None |
| # Extract toolchain part |
| if original_str.endswith(")"): |
| toolchain_begin = original_str.rfind("(") |
| if toolchain_begin != -1: |
| toolchain_str = original_str[toolchain_begin + 1 : -1] |
| toolchain = GnLabel.from_str(toolchain_str) |
| label = original_str[0:toolchain_begin] |
| |
| path, colon, name = label[2:].partition(":") |
| if ".." in path: |
| path = GnLabel._resolve_dot_dot(path) |
| |
| if colon: |
| is_local_name = True |
| gn_str = f"//{path}:{name}" |
| else: |
| name = os.path.basename(path) |
| is_local_name = False |
| gn_str = f"//{path}" |
| |
| if toolchain: |
| gn_str += f"({toolchain.gn_str})" |
| |
| return GnLabel( |
| gn_str=gn_str, |
| name=name, |
| is_local_name=is_local_name, |
| path_str=path, |
| toolchain=toolchain, |
| ) |
| |
| @staticmethod |
| def from_path(path: Path | str) -> "GnLabel": |
| """Constructs a GnLabel instance from a Path object.""" |
| if isinstance(path, Path): |
| if path.name == "" and path.parent.name == "": |
| return GnLabel.from_str("//") |
| return GnLabel.from_str(f"//{path}") |
| elif isinstance(path, str): |
| if ( |
| os.path.basename(path) == "" |
| and os.path.basename(os.path.dirname(path)) == "" |
| ): |
| return GnLabel.from_str("//") |
| return GnLabel.from_str(f"//{path}") |
| else: |
| assert False, f"Expected path of type Path but got {type(path)}" |
| |
| @staticmethod |
| def check_type(other: Any) -> "GnLabel": |
| """Asserts that `other` is of type GnLabel""" |
| assert isinstance( |
| other, GnLabel |
| ), f"{other} type {type(other)} is not {GnLabel}" |
| return other |
| |
| @staticmethod |
| def check_types_in_list(list: List["GnLabel"]) -> List["GnLabel"]: |
| """Asserts that all values in `list` are of type GnLabel""" |
| for v in list: |
| GnLabel.check_type(v) |
| return list |
| |
| def parent_label(self) -> "GnLabel": |
| """Returns //foo/bar for //foo/bar/baz and //foo/bar:baz""" |
| assert self.has_parent_label(), f"{self} has no parent label" |
| if not self.is_local_name and self.name == os.path.basename( |
| self.path_str |
| ): |
| # Return //foo for //foo/bar |
| return GnLabel.from_path(os.path.dirname(self.path_str)) |
| else: |
| # Return //foo/bar for //foo/bar:bar and //foo/bar:baz |
| return GnLabel.from_path(self.path_str) |
| |
| def has_parent_label(self) -> bool: |
| return self.gn_str != "//" |
| |
| def without_toolchain(self) -> "GnLabel": |
| """Removes the toolchain part of the label""" |
| if self.toolchain: |
| return GnLabel.from_str(self.gn_str.split("(", maxsplit=1)[0]) |
| else: |
| return self |
| |
| def ensure_toolchain(self, toolchain: "GnLabel") -> "GnLabel": |
| """Sets a toolchain if there is none already""" |
| if self.toolchain: |
| return self |
| else: |
| return dataclasses.replace( |
| self, toolchain=toolchain, gn_str=f"{self.gn_str}({toolchain})" |
| ) |
| |
| def rebased_path(self, base_dir: Path) -> Path: |
| """Returns package_path rebased to a given base_dir.""" |
| return base_dir / self.path_str |
| |
| def code_search_url(self) -> str: |
| """Returns package_path rebased to a given base_dir.""" |
| return f"https://cs.opensource.google/fuchsia/fuchsia/+/main:{self.path_str}" |
| |
| def is_host_target(self) -> bool: |
| return bool(self.toolchain and self.toolchain.name.startswith("host_")) |
| |
| def is_3rd_party(self) -> bool: |
| return "third_party" in self.gn_str or "thirdparty" in self.gn_str |
| |
| def is_prebuilt(self) -> bool: |
| return "prebuilt" in self.gn_str |
| |
| def is_3p_rust_crate(self) -> bool: |
| return self.gn_str.startswith("//third_party/rust_crates:") |
| |
| def is_3p_golib(self) -> bool: |
| return self.gn_str.startswith("//third_party/golibs:") |
| |
| def is_spdx_json_document(self) -> bool: |
| return self.name.endswith(".spdx.json") |
| |
| def create_child_from_str(self, child_path_str: str) -> "GnLabel": |
| """Create a GnLabel relative to this label from a child path GN string""" |
| if child_path_str.startswith("//"): |
| return GnLabel.from_str(child_path_str) |
| elif child_path_str.startswith(":"): |
| assert ( |
| not self.is_local_name |
| ), f"Can't apply {child_path_str} to {self} because both have :" |
| return GnLabel.from_str(f"//{self.path_str}:{child_path_str[1:]}") |
| elif child_path_str.startswith("../"): |
| parent_path = ( |
| self.parent_label().path_str |
| if self.is_local_name |
| else self.path_str |
| ) |
| return GnLabel.from_path(os.path.join(parent_path, child_path_str)) |
| else: |
| return GnLabel.from_path( |
| os.path.join(self.path_str, child_path_str) |
| ) |
| |
| def __str__(self) -> str: |
| return self.gn_str |
| |
| def __repr__(self) -> str: |
| return f"{self.__class__.__name__}({self.gn_str})" |
| |
| def __gt__(self, other: "GnLabel") -> bool: |
| return self.gn_str > other.gn_str |
| |
| @staticmethod |
| def _resolve_dot_dot(path: str) -> str: |
| """Resolves .. elements in a path string""" |
| assert "//" not in path |
| input_parts = path.split("/") |
| output_parts = [] |
| for part in input_parts: |
| if part != "..": |
| output_parts.append(part) |
| else: |
| assert ( |
| len(output_parts) > 0 |
| ), f".. goes back beyond the base path: {path}" |
| output_parts.pop() |
| return "/".join(output_parts) |