#!/usr/bin/env python # # Copyright (C) 2022 The Android Open Source Project # # 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 # # http://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. from abc import ABC, abstractmethod from collections.abc import Iterator from typing import List TAB = " " class Node(ABC): '''An abstract class that can be serialized to a ninja file All other ninja-serializable classes inherit from this class''' @abstractmethod def stream(self) -> Iterator[str]: pass class Variable(Node): '''A ninja variable that can be reused across build actions https://ninja-build.org/manual.html#_variables''' def __init__(self, name:str, value:str, indent=0): self.name = name self.value = value self.indent = indent def stream(self) -> Iterator[str]: indent = TAB * self.indent yield f"{indent}{self.name} = {self.value}" class RuleException(Exception): pass # Ninja rules recognize a limited set of variables # https://ninja-build.org/manual.html#ref_rule # Keep this list sorted RULE_VARIABLES = ["command", "depfile", "deps", "description", "dyndep", "generator", "msvc_deps_prefix", "restat", "rspfile", "rspfile_content"] class Rule(Node): '''A shorthand for a command line that can be reused https://ninja-build.org/manual.html#_rules''' def __init__(self, name:str): self.name = name self.variables = [] def add_variable(self, name: str, value: str): if name not in RULE_VARIABLES: raise RuleException(f"{name} is not a recognized variable in a ninja rule") self.variables.append(Variable(name=name, value=value, indent=1)) def stream(self) -> Iterator[str]: self._validate_rule() yield f"rule {self.name}" # Yield rule variables sorted by `name` for var in sorted(self.variables, key=lambda x: x.name): # variables yield a single item, next() is sufficient yield next(var.stream()) def _validate_rule(self): # command is a required variable in a ninja rule self._assert_variable_is_not_empty(variable_name="command") def _assert_variable_is_not_empty(self, variable_name: str): if not any(var.name == variable_name for var in self.variables): raise RuleException(f"{variable_name} is required in a ninja rule") class BuildActionException(Exception): pass class BuildAction(Node): '''Describes the dependency edge between inputs and output https://ninja-build.org/manual.html#_build_statements''' def __init__(self, output: str, rule: str, inputs: List[str]=None, implicits: List[str]=None, order_only: List[str]=None): self.output = output self.rule = rule self.inputs = self._as_list(inputs) self.implicits = self._as_list(implicits) self.order_only = self._as_list(order_only) self.variables = [] def add_variable(self, name: str, value: str): '''Variables limited to the scope of this build action''' self.variables.append(Variable(name=name, value=value, indent=1)) def stream(self) -> Iterator[str]: self._validate() build_statement = f"build {self.output}: {self.rule}" if len(self.inputs) > 0: build_statement += " " build_statement += " ".join(self.inputs) if len(self.implicits) > 0: build_statement += " | " build_statement += " ".join(self.implicits) if len(self.order_only) > 0: build_statement += " || " build_statement += " ".join(self.order_only) yield build_statement # Yield variables sorted by `name` for var in sorted(self.variables, key=lambda x: x.name): # variables yield a single item, next() is sufficient yield next(var.stream()) def _validate(self): if not self.output: raise BuildActionException("Output is required in a ninja build statement") if not self.rule: raise BuildActionException("Rule is required in a ninja build statement") def _as_list(self, list_like): if list_like is None: return [] if isinstance(list_like, list): return list_like return [list_like] class Pool(Node): '''https://ninja-build.org/manual.html#ref_pool''' def __init__(self, name: str, depth: int): self.name = name self.depth = Variable(name="depth", value=depth, indent=1) def stream(self) -> Iterator[str]: yield f"pool {self.name}" yield next(self.depth.stream()) class Subninja(Node): def __init__(self, subninja: str, chDir: str): self.subninja = subninja self.chDir = chDir # TODO(spandandas): Update the syntax when aosp/2064612 lands def stream(self) -> Iterator[str]: yield f"subninja {self.subninja}" class Line(Node): '''Generic class that can be used for comments/newlines/default_target etc''' def __init__(self, value:str): self.value = value def stream(self) -> Iterator[str]: yield self.value