• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Utilities for manipulating GN labels and paths."""
15
16from __future__ import annotations
17
18import re
19
20from pathlib import PurePosixPath
21
22
23class MalformedGnError(Exception):
24    """Raised when creating a GN object fails."""
25
26
27class GnPath:
28    """Represents a GN source path to a file in the source tree."""
29
30    def __init__(
31        self,
32        base: str | PurePosixPath | GnPath,
33        bazel: str | None = None,
34        gn: str | None = None,  # pylint: disable=invalid-name
35    ) -> None:
36        """Creates a GN source path.
37
38        Args:
39            base: A base GN source path. Other parameters are used to define
40                this object relative to the base.
41            bazel: A Bazel path relative to `base`.
42            gn: A GN source path relative to `base`.
43        """
44        self._path: PurePosixPath
45        base_path = _as_path(base)
46        if bazel:
47            self._from_bazel(base_path, bazel)
48        elif gn:
49            self._from_gn(base_path, gn)
50        else:
51            self._path = base_path
52
53    def __str__(self) -> str:
54        return str(self._path)
55
56    def _from_bazel(self, base_path: PurePosixPath, label: str) -> None:
57        """Populates this object using a Bazel file label.
58
59        A Bazel label looks like:
60            //[{package-path}][:{relative-path}]
61        e.g.
62            "//foo"             => "$base/foo"
63            "//:bar/baz.txt"    => "$base/bar/baz.txt"
64            "//foo:bar/baz.txt" => "$base/foo/bar/baz.txt"
65        """
66        match = re.match(r'//([^():]*)(?::([^():]+))?', label)
67        if not match:
68            raise MalformedGnError(f'invalid path: {label}')
69        groups = filter(None, match.groups())
70        self._path = base_path.joinpath(*groups)
71
72    def _from_gn(self, base_path: PurePosixPath, path: str) -> None:
73        """Populates this object using a GN label.
74
75        Source-relative paths interpreted relative to `base`. Source-absolute
76        paths are used directly.
77        """
78        if path.startswith('//'):
79            self._path = PurePosixPath(path)
80        else:
81            self._path = base_path.joinpath(path)
82
83    def path(self) -> str:
84        """Returns the object's path."""
85        return str(self._path)
86
87    def file(self) -> str:
88        """Like GN's `get_path_info(..., "file")`."""
89        return self._path.name
90
91    def name(self) -> str:
92        """Like GN's `get_path_info(..., "name")`."""
93        return self._path.stem
94
95    def extension(self) -> str:
96        """Like GN's `get_path_info(..., "extension")`."""
97        suffix = self._path.suffix
98        return suffix[1:] if suffix.startswith('.') else suffix
99
100    def dir(self) -> str:
101        """Like GN's `get_path_info(..., "dir")`."""
102        return str(self._path.parent)
103
104
105class GnLabel:
106    """Represents a GN dependency.
107
108    See https://gn.googlesource.com/gn/+/main/docs/reference.md#labels.
109    """
110
111    def __init__(
112        self,
113        base: str | PurePosixPath | GnLabel,
114        public: bool = False,
115        bazel: str | None = None,
116        gn: str | None = None,  # pylint: disable=invalid-name
117    ) -> None:
118        """Creates a GN label.
119
120        Args:
121            base: A base GN label. Other parameters are used to define this
122                object relative to the base.
123            public: When this label is used to refer to a GN `dep`, this flag
124                indicates if it should be a `public_dep`.
125            bazel: A Bazel label relative to `base`.
126            gn: A GN label relative to `base`.
127        """
128        self._name: str
129        self._path: PurePosixPath
130        self._toolchain: str | None = None
131        self._public: bool = public
132        self._repo: str | None = None
133        base_path = _as_path(base)
134        if bazel:
135            self._from_bazel(base_path, bazel)
136        elif gn:
137            self._from_gn(base_path, gn)
138        elif ':' in str(base_path):
139            parts = str(base_path).split(':')
140            self._path = PurePosixPath(':'.join(parts[:-1]))
141            self._name = parts[-1]
142        elif isinstance(base, GnLabel):
143            self._path = base._path
144            self._name = base._name
145        else:
146            self._path = base_path
147            self._name = self._path.name
148
149    def __str__(self):
150        return self.with_toolchain() if self._toolchain else self.no_toolchain()
151
152    def __eq__(self, other):
153        return str(self) == str(other)
154
155    def __hash__(self):
156        return hash(str(self))
157
158    def _from_bazel(self, base: PurePosixPath, label: str):
159        """Populates this object using a Bazel label."""
160        match = re.match(r'(?:@([^():/]*))?//(.+)', label)
161        if not match:
162            raise MalformedGnError(f'invalid label: {label}')
163        self._repo = match[1]
164        if self._repo:
165            self._from_gn(PurePosixPath('$repo'), match[2])
166        else:
167            self._from_gn(base, match[2])
168
169    def _from_gn(self, base: PurePosixPath, label: str):
170        """Populates this object using a GN label."""
171        if label.startswith('//') or label.startswith('$'):
172            path = label
173        else:
174            path = str(base.joinpath(label))
175        if ':' in path:
176            parts = path.split(':')
177            self._path = PurePosixPath(':'.join(parts[:-1]))
178            self._name = parts[-1]
179        else:
180            self._path = PurePosixPath(path)
181            self._name = self._path.name
182        parts = []
183        for part in self._path.parts:
184            if part == '..' and parts and parts[-1] != '..':
185                parts.pop()
186            else:
187                parts.append(part)
188        self._path = PurePosixPath(*parts)
189
190    def name(self) -> str:
191        """Like GN's `get_label_info(..., "name"`)."""
192        return self._name
193
194    def dir(self) -> str:
195        """Like GN's `get_label_info(..., "dir"`)."""
196        return str(self._path)
197
198    def no_toolchain(self) -> str:
199        """Like GN's `get_label_info(..., "label_no_toolchain"`)."""
200        if self._path == PurePosixPath():
201            return f':{self._name}'
202        name = f':{self._name}' if self._name != self._path.name else ''
203        return f'{self._path}{name}'
204
205    def with_toolchain(self) -> str:
206        """Like GN's `get_label_info(..., "label_with_toolchain"`)."""
207        toolchain = self._toolchain if self._toolchain else 'default_toolchain'
208        if self._path == PurePosixPath():
209            return f':{self._name}({toolchain})'
210        name = f':{self._name}' if self._name != self._path.name else ''
211        return f'{self._path}{name}({toolchain})'
212
213    def public(self) -> bool:
214        """Returns whether this is a public dep."""
215        return self._public
216
217    def repo(self) -> str:
218        """Returns the label's repo, if any."""
219        return self._repo or ''
220
221    def resolve_repo(self, repo: str) -> None:
222        """Replaces the repo placeholder with the given value."""
223        if self._path and self._path.parts[0] == '$repo':
224            self._path = PurePosixPath(
225                '$dir_pw_third_party', repo, *self._path.parts[1:]
226            )
227
228    def relative_to(self, start: str | PurePosixPath | GnLabel) -> str:
229        """Returns a label string relative to the given starting label."""
230        start_path = _as_path(start)
231        if not start:
232            return self.no_toolchain()
233        if self._path == start_path:
234            return f':{self._name}'
235        path = _relative_to(self._path, start_path)
236        name = f':{self._name}' if self._name != self._path.name else ''
237        return f'{path}{name}'
238
239    def joinlabel(self, relative: str) -> GnLabel:
240        """Creates a new label by extending the current label."""
241        return GnLabel(self._path.joinpath(relative))
242
243
244class GnVisibility:
245    """Represents a GN visibility scope."""
246
247    def __init__(
248        self,
249        base: str | PurePosixPath | GnLabel,
250        label: str | PurePosixPath | GnLabel,
251        bazel: str | None = None,
252        gn: str | None = None,  # pylint: disable=invalid-name
253    ) -> None:
254        """Creates a GN visibility scope.
255
256        Args:
257            base: A base GN label. Other parameters are used to define this
258                object relative to the base.
259            label: The label of the directory in which this scope is being
260                defined.
261            bazel: An absolute Bazel visibility label.
262            gn: A GN visibility label.
263        """
264        self._scope: GnLabel
265        label_path = _as_path(label)
266        if bazel:
267            self._from_bazel(_as_path(base), label_path, bazel)
268        elif gn:
269            self._from_gn(label_path, gn)
270        else:
271            self._scope = GnLabel(label)
272
273    def __str__(self):
274        return str(self._scope)
275
276    def _from_bazel(
277        self, base: PurePosixPath, label: PurePosixPath, scope: str
278    ):
279        """Populates this object using a Bazel visibility label."""
280        if scope == '//visibility:public':
281            self._scope = GnLabel('//*')
282        elif scope == '//visibility:private':
283            self._scope = GnLabel(label, gn=':*')
284        elif not (match := re.match(r'//([^():]*):([^():]+)', scope)):
285            raise MalformedGnError(f'invalid visibility scope: {scope}')
286        elif match[2] == '__subpackages__':
287            self._scope = GnLabel(base, gn=f'{match[1]}/*')
288        elif match[2] == '__pkg__':
289            self._scope = GnLabel(base, gn=f'{match[1]}:*')
290        else:
291            raise MalformedGnError(f'unsupported visibility scope: {scope}')
292
293    def _from_gn(self, label: PurePosixPath, scope: str):
294        """Populates this object using a GN visibility scope."""
295        self._scope = GnLabel(label, gn=scope)
296
297    def relative_to(self, start: str | PurePosixPath | GnLabel) -> str:
298        """Returns a label string relative to the given starting label."""
299        return self._scope.relative_to(start)
300
301    def within(self, other: GnVisibility) -> bool:
302        """Returns whether this scope is a subset of another."""
303        as_label = GnLabel(str(other))
304        if as_label.name() == '*':
305            _path = self._scope.dir()
306            other_path = as_label.dir()
307            if other_path == '//*':
308                return True
309            if other_path.endswith('*'):
310                parent = PurePosixPath(other_path).parent
311                return PurePosixPath(_path).is_relative_to(parent)
312            return _path == other_path
313        return str(self) == str(other)
314
315
316def _as_path(item: str | GnPath | GnLabel | PurePosixPath) -> PurePosixPath:
317    """Converts an argument to be a PurePosixPath.
318
319    Args:
320        label: A string, path, or label to be converted to a PurePosixPath.
321    """
322    if isinstance(item, str):
323        return PurePosixPath(item)
324    if isinstance(item, GnPath):
325        return PurePosixPath(item.path())
326    if isinstance(item, GnLabel):
327        return PurePosixPath(item.dir())
328    return item
329
330
331def _relative_to(
332    path: str | PurePosixPath, start: str | PurePosixPath
333) -> PurePosixPath:
334    """Like `PosixPath._relative_to`, but can ascend directories as well."""
335    if not start:
336        return PurePosixPath(path)
337    _path = PurePosixPath(path)
338    _start = PurePosixPath(start)
339    if _path.parts[0] != _start.parts[0]:
340        return _path
341    ascend = PurePosixPath()
342    while not _path.is_relative_to(_start):
343        if _start.parent == PurePosixPath():
344            break
345        _start = _start.parent
346        ascend = ascend.joinpath('..')
347    return ascend.joinpath(_path.relative_to(_start))
348