class NinjaDumper(object): def __init__(self, build_dir, top_build_dir=""): self._build_dir = build_dir.rstrip("/") if top_build_dir: self._top_build_dir = top_build_dir.rstrip("/") else: self._top_build_dir = self._build_dir self._build_file = None self._decl_file = None self._dumper = None self._build_count = None self._subgenerator_count = None def build_pathname(self): return os.path.join(self._build_dir, config.BUILD_FILENAME) def decl_pathname(self): return os.path.join(self._build_dir, config.DECL_FILENAME) def __enter__(self): self.open() return self def __exit__(self, exc_type, exc_value, traceback): self.close() if exc_type is None: # No exception was raised so we can safely apply changes. self.apply() return False # Tell to re-raise the exception if there was one. def open(self): """Open all the files that will be written by this dumper.""" self._build_file = open(partfile(self.build_pathname()), "w") self._decl_file = open(partfile(self.decl_pathname()), "w") self._build_writer = NinjaWriter(self._build_file) self._decl_writer = NinjaWriter(self._decl_file) _write_caution_header(self._build_writer) _write_caution_header(self._decl_writer) self._write_build_header() # Keep track of added rule names because we dump rule only once. self._rules = {} # Keep track of added pool names because we dump them only once. self._pools = set() self._build_count = 0 self._subgenerator_count = 0 def close(self): """Close all the files opened by this dumper.""" self._build_file.close() self._decl_file.close() self._build_file = None self._decl_file = None self._build_writer = None self._decl_writer = None def apply(self): """Apply the new generated files so that ninja can see them.""" # TODO(Nicolas Despres): Make this function atomic w.r.t signals. for pathname in (self.build_pathname(), self.decl_pathname()): os.rename(partfile(pathname), pathname) def is_opened(self): return self._build_file is not None \ and self._decl_file is not None def _check_is_opened(self): if not self.is_opened(): raise RuntimeError("dumper not opened") def _write_build_header(self): self._build_writer.newline() self._build_writer.variable("ninja_required_version", "1.6") self._build_writer.newline() self._build_writer.include(self.decl_pathname()) def dump(self, obj): # TODO(Nicolas Despres): Catch raised exception to provide some context. self._check_is_opened() if hasattr(obj, "generate"): # TODO(Nicolas Despres): Redirect stdout,stderr to a logger of # some kind. obj.generate() if isinstance(obj, Command): self._dump_command(obj) elif isinstance(obj, SubGenerator): self._dump_subgenerator(obj) elif isinstance(obj, Phony): self._dump_phony(obj) elif isinstance(obj, Default): self._dump_default(obj) else: raise TypeError("cannot dump {} object: {!r}" .format(type(obj).__name__, obj)) @property def rule_count(self): return len(self._rules) @property def build_count(self): return self._build_count @property def subgenerator_count(self): return self._subgenerator_count def _add_rule(self, rule): """Register the rule if it is not and compute its name. Return whether the rule has been added. """ try: name = self._rules[rule] except KeyError: rule.name = "r{}".format(len(self._rules)) self._rules[rule] = rule.name return True else: rule.name = name return False def _dump_command(self, cmd): if not cmd.has_outputs(): raise ValueError("no outputs for command '{}'".format(cmd)) if type(cmd).__name__ == "phony": raise ValueError("command class cannot be named 'phony'") if _is_custom_pool(cmd.pool): pool_name = _pool_name(cmd.pool) if pool_name not in self._pools: self._pools.add(pool_name) self._dump_pool_statement(cmd.pool) rule = _cmd_rule(cmd) if self._add_rule(rule): self._dump_rule_statement(rule) self._dump_build_statement(rule, cmd) self._build_count += 1 def _dump_pool_statement(self, pool): self._decl_writer.newline() if pool.__doc__: self._decl_writer.raw_comment(_dedent_docstring(pool.__doc__)) self._decl_writer.pool(_pool_name(pool), pool.depth) def _dump_rule_statement(self, rule): self._decl_writer.newline() if rule.doc: self._decl_writer.raw_comment(_dedent_docstring(rule.doc)) if not rule.expanded_command(): raise ValueError("empty command string") self._decl_writer.rule( rule.name, rule.command, description=rule.description, generator=rule.generator, restat=rule.restat, pool=rule.pool) def _dump_build_statement(self, rule, cmd): self._build_writer.newline() self._build_writer.build( self._format_targets(cmd.all_outputs()), rule.name, inputs=self._format_targets(cmd.inputs), implicit=self._format_targets(cmd.implicit_inputs), order_only=self._format_targets(cmd.orderonly_inputs), variables=rule.variables) def _dump_subgenerator(self, subgen): self._build_writer.comment("{name} sub-project from '{source}'" .format(name=type(subgen).__name__, source=subgen.source), has_path=True) self._build_writer.subninja(subgen.output) self._build_writer.newline() self._subgenerator_count += 1 def _format_targets(self, filenames): def f(target): target = getattr(target, "build_edge_key", target) # We convert targets to str so that object like pathlib.Path are # accepted. return self._relative_to_build_dir(str(target)) return sorted(map(f, filenames)) def _relative_to_build_dir(self, path): if path.startswith(self._top_build_dir): assert not self._top_build_dir.endswith("/") return path[len(self._top_build_dir)+1:] return path def _dump_phony(self, phony): self._build_writer.newline() self._build_writer.build( self._format_targets(phony.outputs), "phony", inputs=self._format_targets(phony.inputs), implicit=self._format_targets(phony.implicit_inputs), order_only=self._format_targets(phony.orderonly_inputs)) def _dump_default(self, default): self._build_writer.newline() self._build_writer.default(self._format_targets(default.targets))