| # Copyright 2023 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Utilities for manipulating GN labels and paths.""" |
| |
| from __future__ import annotations |
| |
| import re |
| |
| from pathlib import PurePosixPath |
| |
| |
| class MalformedGnError(Exception): |
| """Raised when creating a GN object fails.""" |
| |
| |
| class GnPath: |
| """Represents a GN source path to a file in the source tree.""" |
| |
| def __init__( |
| self, |
| base: str | PurePosixPath | GnPath, |
| bazel: str | None = None, |
| gn: str | None = None, # pylint: disable=invalid-name |
| ) -> None: |
| """Creates a GN source path. |
| |
| Args: |
| base: A base GN source path. Other parameters are used to define |
| this object relative to the base. |
| bazel: A Bazel path relative to `base`. |
| gn: A GN source path relative to `base`. |
| """ |
| self._path: PurePosixPath |
| base_path = _as_path(base) |
| if bazel: |
| self._from_bazel(base_path, bazel) |
| elif gn: |
| self._from_gn(base_path, gn) |
| else: |
| self._path = base_path |
| |
| def __str__(self) -> str: |
| return str(self._path) |
| |
| def _from_bazel(self, base_path: PurePosixPath, label: str) -> None: |
| """Populates this object using a Bazel file label. |
| |
| A Bazel label looks like: |
| //[{package-path}][:{relative-path}] |
| e.g. |
| "//foo" => "$base/foo" |
| "//:bar/baz.txt" => "$base/bar/baz.txt" |
| "//foo:bar/baz.txt" => "$base/foo/bar/baz.txt" |
| """ |
| match = re.match(r'//([^():]*)(?::([^():]+))?', label) |
| if not match: |
| raise MalformedGnError(f'invalid path: {label}') |
| groups = filter(None, match.groups()) |
| self._path = base_path.joinpath(*groups) |
| |
| def _from_gn(self, base_path: PurePosixPath, path: str) -> None: |
| """Populates this object using a GN label. |
| |
| Source-relative paths interpreted relative to `base`. Source-absolute |
| paths are used directly. |
| """ |
| if path.startswith('//'): |
| self._path = PurePosixPath(path) |
| else: |
| self._path = base_path.joinpath(path) |
| |
| def path(self) -> str: |
| """Returns the object's path.""" |
| return str(self._path) |
| |
| def file(self) -> str: |
| """Like GN's `get_path_info(..., "file")`.""" |
| return self._path.name |
| |
| def name(self) -> str: |
| """Like GN's `get_path_info(..., "name")`.""" |
| return self._path.stem |
| |
| def extension(self) -> str: |
| """Like GN's `get_path_info(..., "extension")`.""" |
| suffix = self._path.suffix |
| return suffix[1:] if suffix.startswith('.') else suffix |
| |
| def dir(self) -> str: |
| """Like GN's `get_path_info(..., "dir")`.""" |
| return str(self._path.parent) |
| |
| |
| class GnLabel: |
| """Represents a GN dependency. |
| |
| See https://gn.googlesource.com/gn/+/main/docs/reference.md#labels. |
| """ |
| |
| def __init__( |
| self, |
| base: str | PurePosixPath | GnLabel, |
| public: bool = False, |
| bazel: str | None = None, |
| gn: str | None = None, # pylint: disable=invalid-name |
| ) -> None: |
| """Creates a GN label. |
| |
| Args: |
| base: A base GN label. Other parameters are used to define this |
| object relative to the base. |
| public: When this label is used to refer to a GN `dep`, this flag |
| indicates if it should be a `public_dep`. |
| bazel: A Bazel label relative to `base`. |
| gn: A GN label relative to `base`. |
| """ |
| self._name: str |
| self._path: PurePosixPath |
| self._toolchain: str | None = None |
| self._public: bool = public |
| self._repo: str | None = None |
| base_path = _as_path(base) |
| if bazel: |
| self._from_bazel(base_path, bazel) |
| elif gn: |
| self._from_gn(base_path, gn) |
| elif ':' in str(base_path): |
| parts = str(base_path).split(':') |
| self._path = PurePosixPath(':'.join(parts[:-1])) |
| self._name = parts[-1] |
| elif isinstance(base, GnLabel): |
| self._path = base._path |
| self._name = base._name |
| else: |
| self._path = base_path |
| self._name = self._path.name |
| |
| def __str__(self): |
| return self.with_toolchain() if self._toolchain else self.no_toolchain() |
| |
| def __eq__(self, other): |
| return str(self) == str(other) |
| |
| def __hash__(self): |
| return hash(str(self)) |
| |
| def _from_bazel(self, base: PurePosixPath, label: str): |
| """Populates this object using a Bazel label.""" |
| match = re.match(r'(?:@([^():/]*))?//(.+)', label) |
| if not match: |
| raise MalformedGnError(f'invalid label: {label}') |
| self._repo = match[1] |
| if self._repo: |
| self._from_gn(PurePosixPath('$repo'), match[2]) |
| else: |
| self._from_gn(base, match[2]) |
| |
| def _from_gn(self, base: PurePosixPath, label: str): |
| """Populates this object using a GN label.""" |
| if label.startswith('//') or label.startswith('$'): |
| path = label |
| else: |
| path = str(base.joinpath(label)) |
| if ':' in path: |
| parts = path.split(':') |
| self._path = PurePosixPath(':'.join(parts[:-1])) |
| self._name = parts[-1] |
| else: |
| self._path = PurePosixPath(path) |
| self._name = self._path.name |
| parts = [] |
| for part in self._path.parts: |
| if part == '..' and parts and parts[-1] != '..': |
| parts.pop() |
| else: |
| parts.append(part) |
| self._path = PurePosixPath(*parts) |
| |
| def name(self) -> str: |
| """Like GN's `get_label_info(..., "name"`).""" |
| return self._name |
| |
| def dir(self) -> str: |
| """Like GN's `get_label_info(..., "dir"`).""" |
| return str(self._path) |
| |
| def no_toolchain(self) -> str: |
| """Like GN's `get_label_info(..., "label_no_toolchain"`).""" |
| if self._path == PurePosixPath(): |
| return f':{self._name}' |
| name = f':{self._name}' if self._name != self._path.name else '' |
| return f'{self._path}{name}' |
| |
| def with_toolchain(self) -> str: |
| """Like GN's `get_label_info(..., "label_with_toolchain"`).""" |
| toolchain = self._toolchain if self._toolchain else 'default_toolchain' |
| if self._path == PurePosixPath(): |
| return f':{self._name}({toolchain})' |
| name = f':{self._name}' if self._name != self._path.name else '' |
| return f'{self._path}{name}({toolchain})' |
| |
| def public(self) -> bool: |
| """Returns whether this is a public dep.""" |
| return self._public |
| |
| def repo(self) -> str: |
| """Returns the label's repo, if any.""" |
| return self._repo or '' |
| |
| def resolve_repo(self, repo: str) -> None: |
| """Replaces the repo placeholder with the given value.""" |
| if self._path and self._path.parts[0] == '$repo': |
| self._path = PurePosixPath( |
| '$dir_pw_third_party', repo, *self._path.parts[1:] |
| ) |
| |
| def relative_to(self, start: str | PurePosixPath | GnLabel) -> str: |
| """Returns a label string relative to the given starting label.""" |
| start_path = _as_path(start) |
| if not start: |
| return self.no_toolchain() |
| if self._path == start_path: |
| return f':{self._name}' |
| path = _relative_to(self._path, start_path) |
| name = f':{self._name}' if self._name != self._path.name else '' |
| return f'{path}{name}' |
| |
| def joinlabel(self, relative: str) -> GnLabel: |
| """Creates a new label by extending the current label.""" |
| return GnLabel(self._path.joinpath(relative)) |
| |
| |
| class GnVisibility: |
| """Represents a GN visibility scope.""" |
| |
| def __init__( |
| self, |
| base: str | PurePosixPath | GnLabel, |
| label: str | PurePosixPath | GnLabel, |
| bazel: str | None = None, |
| gn: str | None = None, # pylint: disable=invalid-name |
| ) -> None: |
| """Creates a GN visibility scope. |
| |
| Args: |
| base: A base GN label. Other parameters are used to define this |
| object relative to the base. |
| label: The label of the directory in which this scope is being |
| defined. |
| bazel: An absolute Bazel visibility label. |
| gn: A GN visibility label. |
| """ |
| self._scope: GnLabel |
| label_path = _as_path(label) |
| if bazel: |
| self._from_bazel(_as_path(base), label_path, bazel) |
| elif gn: |
| self._from_gn(label_path, gn) |
| else: |
| self._scope = GnLabel(label) |
| |
| def __str__(self): |
| return str(self._scope) |
| |
| def _from_bazel( |
| self, base: PurePosixPath, label: PurePosixPath, scope: str |
| ): |
| """Populates this object using a Bazel visibility label.""" |
| if scope == '//visibility:public': |
| self._scope = GnLabel('//*') |
| elif scope == '//visibility:private': |
| self._scope = GnLabel(label, gn=':*') |
| elif not (match := re.match(r'//([^():]*):([^():]+)', scope)): |
| raise MalformedGnError(f'invalid visibility scope: {scope}') |
| elif match[2] == '__subpackages__': |
| self._scope = GnLabel(base, gn=f'{match[1]}/*') |
| elif match[2] == '__pkg__': |
| self._scope = GnLabel(base, gn=f'{match[1]}:*') |
| else: |
| raise MalformedGnError(f'unsupported visibility scope: {scope}') |
| |
| def _from_gn(self, label: PurePosixPath, scope: str): |
| """Populates this object using a GN visibility scope.""" |
| self._scope = GnLabel(label, gn=scope) |
| |
| def relative_to(self, start: str | PurePosixPath | GnLabel) -> str: |
| """Returns a label string relative to the given starting label.""" |
| return self._scope.relative_to(start) |
| |
| def within(self, other: GnVisibility) -> bool: |
| """Returns whether this scope is a subset of another.""" |
| as_label = GnLabel(str(other)) |
| if as_label.name() == '*': |
| _path = self._scope.dir() |
| other_path = as_label.dir() |
| if other_path == '//*': |
| return True |
| if other_path.endswith('*'): |
| parent = PurePosixPath(other_path).parent |
| return PurePosixPath(_path).is_relative_to(parent) |
| return _path == other_path |
| return str(self) == str(other) |
| |
| |
| def _as_path(item: str | GnPath | GnLabel | PurePosixPath) -> PurePosixPath: |
| """Converts an argument to be a PurePosixPath. |
| |
| Args: |
| label: A string, path, or label to be converted to a PurePosixPath. |
| """ |
| if isinstance(item, str): |
| return PurePosixPath(item) |
| if isinstance(item, GnPath): |
| return PurePosixPath(item.path()) |
| if isinstance(item, GnLabel): |
| return PurePosixPath(item.dir()) |
| return item |
| |
| |
| def _relative_to( |
| path: str | PurePosixPath, start: str | PurePosixPath |
| ) -> PurePosixPath: |
| """Like `PosixPath._relative_to`, but can ascend directories as well.""" |
| if not start: |
| return PurePosixPath(path) |
| _path = PurePosixPath(path) |
| _start = PurePosixPath(start) |
| if _path.parts[0] != _start.parts[0]: |
| return _path |
| ascend = PurePosixPath() |
| while not _path.is_relative_to(_start): |
| if _start.parent == PurePosixPath(): |
| break |
| _start = _start.parent |
| ascend = ascend.joinpath('..') |
| return ascend.joinpath(_path.relative_to(_start)) |