251 lines
7.5 KiB
Python
251 lines
7.5 KiB
Python
#!/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 = "",
|
|
env_vars: list[dict] = ()):
|
|
self.subninja = subninja
|
|
self.chdir = chdir
|
|
self.env_vars = env_vars
|
|
|
|
def stream(self) -> Iterator[str]:
|
|
token = f"subninja {self.subninja}"
|
|
for env_var in self.env_vars:
|
|
token += f"\n env {env_var['Key']}={env_var['Value']}"
|
|
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
|