blob: e7c5c039fc873e5ca620d1724b0cd7c0a6cd6b22 [file] [log] [blame]
#!/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
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
def key(self):
"""The value used for equality and hashing.
The inheriting class must define this.
"""
raise NotImplementedError
def __eq__(self, other):
"""Test for equality."""
if isinstance(other, self.__class__):
return self.key() == other.key()
raise NotImplementedError
def __hash__(self):
"""Hash the object."""
return hash(self.key())
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 key(self):
"""The value used for equality and hashing."""
return (self.name, self.value, self.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, variables: list[tuple[(str, str)]] = ()):
self.name = name
self._variables = []
for k, v in variables:
self.add_variable(k, v)
@property
def variables(self):
"""The (sorted) variables for this rule."""
return sorted(self._variables, key=lambda x: x.name)
def key(self):
"""The value used for equality and hashing."""
return (self.name, tuple(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}"
for var in self.variables:
yield from 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,
*args,
rule: str,
output: list[str] = None,
inputs: list[str] = None,
implicits: list[str] = None,
order_only: list[str] = None,
variables: list[tuple[(str, str)]] = ()):
assert not args, "parameters must be passed as keywords"
self.output = self._as_list(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 = []
for k, v in variables:
self.add_variable(k, v)
def key(self):
return (self.output, self.rule, tuple(self.inputs),
tuple(self.implicits), tuple(self.order_only),
tuple(self.variables))
@property
def variables(self):
"""The (sorted) variables for this rule."""
return sorted(self._variables, key=lambda x: x.name)
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()
output = " ".join(self.output)
build_statement = f"build {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
for var in self.variables:
yield from 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):
"""Returns a tuple, after casting the input."""
if isinstance(list_like, (int, bool)):
return tuple([str(list_like)])
# False-ish values that are neither ints nor bools return false-ish.
if not list_like:
return ()
if isinstance(list_like, tuple):
return list_like
if isinstance(list_like, (list, set)):
return tuple(list_like)
if isinstance(list_like, str):
return tuple([list_like])
raise TypeError(f"bad type {type(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 from self.depth.stream()
class Subninja(Node):
def __init__(self, subninja: str, chDir: str):
self.subninja = subninja
self.chDir = chDir
def stream(self) -> Iterator[str]:
token = f"subninja {self.subninja}"
if self.chDir:
token += f"\n chdir = {self.chDir}"
yield token
class Line(Node):
"""Generic single-line node: comments, newlines, default_target etc."""
def __init__(self, value: str):
self.value = value
def stream(self) -> Iterator[str]:
yield self.value