Spandan Das | aacf237 | 2022-05-11 21:46:29 +0000 | [diff] [blame] | 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 | |
| 17 | from abc import ABC, abstractmethod |
| 18 | |
| 19 | from collections.abc import Iterator |
| 20 | from typing import List |
| 21 | |
| 22 | TAB = " " |
| 23 | |
| 24 | class Node(ABC): |
| 25 | '''An abstract class that can be serialized to a ninja file |
| 26 | All other ninja-serializable classes inherit from this class''' |
| 27 | |
| 28 | @abstractmethod |
| 29 | def stream(self) -> Iterator[str]: |
| 30 | pass |
| 31 | |
| 32 | class Variable(Node): |
| 33 | '''A ninja variable that can be reused across build actions |
| 34 | https://ninja-build.org/manual.html#_variables''' |
| 35 | |
| 36 | def __init__(self, name:str, value:str, indent=0): |
| 37 | self.name = name |
| 38 | self.value = value |
| 39 | self.indent = indent |
| 40 | |
| 41 | def stream(self) -> Iterator[str]: |
| 42 | indent = TAB * self.indent |
| 43 | yield f"{indent}{self.name} = {self.value}" |
| 44 | |
| 45 | class RuleException(Exception): |
| 46 | pass |
| 47 | |
| 48 | # Ninja rules recognize a limited set of variables |
| 49 | # https://ninja-build.org/manual.html#ref_rule |
| 50 | # Keep this list sorted |
| 51 | RULE_VARIABLES = ["command", |
| 52 | "depfile", |
| 53 | "deps", |
| 54 | "description", |
| 55 | "dyndep", |
| 56 | "generator", |
| 57 | "msvc_deps_prefix", |
| 58 | "restat", |
| 59 | "rspfile", |
| 60 | "rspfile_content"] |
| 61 | |
| 62 | class Rule(Node): |
| 63 | '''A shorthand for a command line that can be reused |
| 64 | https://ninja-build.org/manual.html#_rules''' |
| 65 | |
| 66 | def __init__(self, name:str): |
| 67 | self.name = name |
| 68 | self.variables = [] |
| 69 | |
| 70 | def add_variable(self, name: str, value: str): |
| 71 | if name not in RULE_VARIABLES: |
| 72 | raise RuleException(f"{name} is not a recognized variable in a ninja rule") |
| 73 | |
| 74 | self.variables.append(Variable(name=name, value=value, indent=1)) |
| 75 | |
| 76 | def stream(self) -> Iterator[str]: |
| 77 | self._validate_rule() |
| 78 | |
| 79 | yield f"rule {self.name}" |
| 80 | # Yield rule variables sorted by `name` |
| 81 | for var in sorted(self.variables, key=lambda x: x.name): |
| 82 | # variables yield a single item, next() is sufficient |
| 83 | yield next(var.stream()) |
| 84 | |
| 85 | def _validate_rule(self): |
| 86 | # command is a required variable in a ninja rule |
| 87 | self._assert_variable_is_not_empty(variable_name="command") |
| 88 | |
| 89 | def _assert_variable_is_not_empty(self, variable_name: str): |
| 90 | if not any(var.name == variable_name for var in self.variables): |
| 91 | raise RuleException(f"{variable_name} is required in a ninja rule") |
| 92 | |
| 93 | class BuildActionException(Exception): |
| 94 | pass |
| 95 | |
| 96 | class BuildAction(Node): |
| 97 | '''Describes the dependency edge between inputs and output |
| 98 | https://ninja-build.org/manual.html#_build_statements''' |
| 99 | |
| 100 | def __init__(self, output: str, rule: str, inputs: List[str]=None, implicits: List[str]=None, order_only: List[str]=None): |
| 101 | self.output = output |
| 102 | self.rule = rule |
| 103 | self.inputs = self._as_list(inputs) |
| 104 | self.implicits = self._as_list(implicits) |
| 105 | self.order_only = self._as_list(order_only) |
| 106 | self.variables = [] |
| 107 | |
| 108 | def add_variable(self, name: str, value: str): |
| 109 | '''Variables limited to the scope of this build action''' |
| 110 | self.variables.append(Variable(name=name, value=value, indent=1)) |
| 111 | |
| 112 | def stream(self) -> Iterator[str]: |
| 113 | self._validate() |
| 114 | |
| 115 | build_statement = f"build {self.output}: {self.rule}" |
| 116 | if len(self.inputs) > 0: |
| 117 | build_statement += " " |
| 118 | build_statement += " ".join(self.inputs) |
| 119 | if len(self.implicits) > 0: |
| 120 | build_statement += " | " |
| 121 | build_statement += " ".join(self.implicits) |
| 122 | if len(self.order_only) > 0: |
| 123 | build_statement += " || " |
| 124 | build_statement += " ".join(self.order_only) |
| 125 | yield build_statement |
| 126 | # Yield variables sorted by `name` |
| 127 | for var in sorted(self.variables, key=lambda x: x.name): |
| 128 | # variables yield a single item, next() is sufficient |
| 129 | yield next(var.stream()) |
| 130 | |
| 131 | def _validate(self): |
| 132 | if not self.output: |
| 133 | raise BuildActionException("Output is required in a ninja build statement") |
| 134 | if not self.rule: |
| 135 | raise BuildActionException("Rule is required in a ninja build statement") |
| 136 | |
| 137 | def _as_list(self, list_like): |
| 138 | if list_like is None: |
| 139 | return [] |
| 140 | if isinstance(list_like, list): |
| 141 | return list_like |
| 142 | return [list_like] |
| 143 | |
| 144 | class Pool(Node): |
| 145 | '''https://ninja-build.org/manual.html#ref_pool''' |
| 146 | |
| 147 | def __init__(self, name: str, depth: int): |
| 148 | self.name = name |
| 149 | self.depth = Variable(name="depth", value=depth, indent=1) |
| 150 | |
| 151 | def stream(self) -> Iterator[str]: |
| 152 | yield f"pool {self.name}" |
| 153 | yield next(self.depth.stream()) |
| 154 | |
| 155 | class Subninja(Node): |
| 156 | |
| 157 | def __init__(self, subninja: str, chDir: str): |
| 158 | self.subninja = subninja |
| 159 | self.chDir = chDir |
| 160 | |
| 161 | # TODO(spandandas): Update the syntax when aosp/2064612 lands |
Joe Onorato | 5149718 | 2022-05-12 12:58:10 -0700 | [diff] [blame] | 162 | def stream(self) -> Iterator[str]: |
Spandan Das | aacf237 | 2022-05-11 21:46:29 +0000 | [diff] [blame] | 163 | yield f"subninja {self.subninja}" |
| 164 | |
| 165 | class Line(Node): |
| 166 | '''Generic class that can be used for comments/newlines/default_target etc''' |
| 167 | |
| 168 | def __init__(self, value:str): |
| 169 | self.value = value |
| 170 | |
| 171 | def stream(self) -> Iterator[str]: |
| 172 | yield self.value |