• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright (C) 2022 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17from abc import ABC, abstractmethod
18
19from collections.abc import Iterator
20
21TAB = "  "
22
23
24class Node(ABC):
25    """An abstract class that can be serialized to a ninja file
26
27    All other ninja-serializable classes inherit from this class
28    """
29
30    @abstractmethod
31    def stream(self) -> Iterator[str]:
32        pass
33
34    def key(self):
35        """The value used for equality and hashing.
36
37        The inheriting class must define this.
38        """
39        raise NotImplementedError
40
41    def __eq__(self, other):
42        """Test for equality."""
43        if isinstance(other, self.__class__):
44            return self.key() == other.key()
45        raise NotImplementedError
46
47    def __hash__(self):
48        """Hash the object."""
49        return hash(self.key())
50
51
52class Variable(Node):
53    """A ninja variable that can be reused across build actions
54
55    https://ninja-build.org/manual.html#_variables
56    """
57
58    def __init__(self, name: str, value: str, indent=0):
59        self.name = name
60        self.value = value
61        self.indent = indent
62
63    def key(self):
64        """The value used for equality and hashing."""
65        return (self.name, self.value, self.indent)
66
67    def stream(self) -> Iterator[str]:
68        indent = TAB * self.indent
69        yield f"{indent}{self.name} = {self.value}"
70
71
72class RuleException(Exception):
73    pass
74
75
76# Ninja rules recognize a limited set of variables
77# https://ninja-build.org/manual.html#ref_rule
78# Keep this list sorted
79RULE_VARIABLES = [
80    "command", "depfile", "deps", "description", "dyndep", "generator",
81    "msvc_deps_prefix", "restat", "rspfile", "rspfile_content"
82]
83
84
85class Rule(Node):
86    """A shorthand for a command line that can be reused
87
88    https://ninja-build.org/manual.html#_rules
89    """
90
91    def __init__(self, name: str, variables: list[tuple[(str, str)]] = ()):
92        self.name = name
93        self._variables = []
94        for k, v in variables:
95            self.add_variable(k, v)
96
97    @property
98    def variables(self):
99        """The (sorted) variables for this rule."""
100        return sorted(self._variables, key=lambda x: x.name)
101
102    def key(self):
103        """The value used for equality and hashing."""
104        return (self.name, tuple(self.variables))
105
106    def add_variable(self, name: str, value: str):
107        if name not in RULE_VARIABLES:
108            raise RuleException(
109                f"{name} is not a recognized variable in a ninja rule")
110
111        self._variables.append(Variable(name=name, value=value, indent=1))
112
113    def stream(self) -> Iterator[str]:
114        self._validate_rule()
115
116        yield f"rule {self.name}"
117        for var in self.variables:
118            yield from var.stream()
119
120    def _validate_rule(self):
121        # command is a required variable in a ninja rule
122        self._assert_variable_is_not_empty(variable_name="command")
123
124    def _assert_variable_is_not_empty(self, variable_name: str):
125        if not any(var.name == variable_name for var in self.variables):
126            raise RuleException(f"{variable_name} is required in a ninja rule")
127
128
129class BuildActionException(Exception):
130    pass
131
132
133class BuildAction(Node):
134    """Describes the dependency edge between inputs and output
135
136    https://ninja-build.org/manual.html#_build_statements
137    """
138
139    def __init__(self,
140                 *args,
141                 rule: str,
142                 output: list[str] = None,
143                 inputs: list[str] = None,
144                 implicits: list[str] = None,
145                 order_only: list[str] = None,
146                 variables: list[tuple[(str, str)]] = ()):
147        assert not args, "parameters must be passed as keywords"
148        self.output = self._as_list(output)
149        self.rule = rule
150        self.inputs = self._as_list(inputs)
151        self.implicits = self._as_list(implicits)
152        self.order_only = self._as_list(order_only)
153        self._variables = []
154        for k, v in variables:
155            self.add_variable(k, v)
156
157    def key(self):
158        return (self.output, self.rule, tuple(self.inputs),
159                tuple(self.implicits), tuple(self.order_only),
160                tuple(self.variables))
161
162    @property
163    def variables(self):
164        """The (sorted) variables for this rule."""
165        return sorted(self._variables, key=lambda x: x.name)
166
167    def add_variable(self, name: str, value: str):
168        """Variables limited to the scope of this build action"""
169        self._variables.append(Variable(name=name, value=value, indent=1))
170
171    def stream(self) -> Iterator[str]:
172        self._validate()
173
174        output = " ".join(self.output)
175        build_statement = f"build {output}: {self.rule}"
176        if len(self.inputs) > 0:
177            build_statement += " "
178            build_statement += " ".join(self.inputs)
179        if len(self.implicits) > 0:
180            build_statement += " | "
181            build_statement += " ".join(self.implicits)
182        if len(self.order_only) > 0:
183            build_statement += " || "
184            build_statement += " ".join(self.order_only)
185        yield build_statement
186        for var in self.variables:
187            yield from var.stream()
188
189    def _validate(self):
190        if not self.output:
191            raise BuildActionException(
192                "Output is required in a ninja build statement")
193        if not self.rule:
194            raise BuildActionException(
195                "Rule is required in a ninja build statement")
196
197    def _as_list(self, list_like):
198        """Returns a tuple, after casting the input."""
199        if isinstance(list_like, (int, bool)):
200            return tuple([str(list_like)])
201        # False-ish values that are neither ints nor bools return false-ish.
202        if not list_like:
203            return ()
204        if isinstance(list_like, tuple):
205            return list_like
206        if isinstance(list_like, (list, set)):
207            return tuple(list_like)
208        if isinstance(list_like, str):
209            return tuple([list_like])
210        raise TypeError(f"bad type {type(list_like)}")
211
212
213class Pool(Node):
214    """https://ninja-build.org/manual.html#ref_pool"""
215
216    def __init__(self, name: str, depth: int):
217        self.name = name
218        self.depth = Variable(name="depth", value=depth, indent=1)
219
220    def stream(self) -> Iterator[str]:
221        yield f"pool {self.name}"
222        yield from self.depth.stream()
223
224
225class Subninja(Node):
226    def __init__(self,
227                 subninja: str,
228                 chdir: str = "",
229                 env_vars: list[dict] = ()):
230        self.subninja = subninja
231        self.chdir = chdir
232        self.env_vars = env_vars
233
234    def stream(self) -> Iterator[str]:
235        token = f"subninja {self.subninja}"
236        for env_var in self.env_vars:
237            token += f"\n  env {env_var['Key']}={env_var['Value']}"
238        if self.chdir:
239            token += f"\n  chdir = {self.chdir}"
240        yield token
241
242
243class Line(Node):
244    """Generic single-line node: comments, newlines, default_target etc."""
245
246    def __init__(self, value: str):
247        self.value = value
248
249    def stream(self) -> Iterator[str]:
250        yield self.value
251