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