Lightweight ninja writer in Python
Summary: - Create python classes for ninja vocbulary in `ninja_syntax.py`. These classes will be serialized to a ninja file - Create a Writer class in `ninja_writer.py`. The current API supports adding variables,rules,build actions, etc. This can be extended in the future (See `test_ninja_writer.py` for examples) Future Work: - Update the `Subninja` class once chDir is supported (aosp/2064612) - Support a width parameter that will be used to wrap long lines of text. This will improve readability of the generated files Expected Use Case: Multi-tree build orchestrator Test: python ./test_ninja_syntax.py Test: python ./test_ninja_writer.py Change-Id: I90c7ee69ddeb7c20c3fd4fca5a911dddbf2253bd
This commit is contained in:
172
orchestrator/ninja/ninja_syntax.py
Normal file
172
orchestrator/ninja/ninja_syntax.py
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/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() -> 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
|
Reference in New Issue
Block a user