| # Copyright 2021 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Python module for creating depfiles that can be read by ninja. |
| |
| ninja supports depfiles for tracking implicit dependencies that are needed by |
| future incremental rebuilds: https://ninja-build.org/manual.html#_depfile |
| |
| The format of these is a Makefile, with a single output listed (see |
| https://gn.googlesource.com/gn/+/main/docs/reference.md#var_depfile) |
| |
| Examples: |
| |
| The simplest form is a single line for output and each inputs:: |
| |
| path/to/an/output: path/to/input_a, path/to/input_b |
| |
| paths are all relative to the root build dir (GN's root_build_dir) |
| |
| Ninja also supports a multiline format which uses backslashes as a continuation |
| character:: |
| |
| path/to/an/output: \ |
| path/to/input_a \ |
| path/to/input_b \ |
| |
| For readability, this is the format that is used by this module. |
| |
| Basic usage: |
| |
| >>> dep_file = DepFile("path/to/output") |
| >>> dep_file.add_input("path/to/input_a") |
| >>> dep_file.add_input("path/to/input_b") |
| >>> print(dep_file) |
| path/to/output: \ |
| path/to/input_a \ |
| path/to/input_b \ |
| |
| >>> |
| |
| By default, paths are made relative to the current working directory, but the |
| paths can all be rebased (made relative from) a different absolute path: |
| |
| Assuming that the current working dir is ``/foo/bar``, and the paths to the |
| output and inputs are relative |
| |
| >>> dep_file = DepFile("baz/melon/output", rebase="/foo/bar/baz") |
| >>> dep_file.add_input("baz/input_a") |
| >>> dep_file.add_input("/foo/bar/baz/input_b") |
| >>> dep_file.add_input("/foo/bar/monkey/panda/input_c") |
| >>> print(dep_file) |
| melon/output: \ |
| input_a input_b \ |
| ../monkey/panda/input/_c \ |
| |
| >>> |
| """ |
| import os |
| import shlex |
| from os import PathLike |
| from typing import Any, Iterable, Self, TextIO, Union |
| |
| FilePath = Union[str, PathLike[Any]] |
| |
| |
| class DepFile: |
| """A helper class for collecting implicit dependencies and writing them to |
| depfiles that ninja can read. |
| |
| Each DepFile instance supports collecting the inputs used to create a single |
| output file. |
| """ |
| |
| def __init__( |
| self, output: FilePath, rebase: None | FilePath = None |
| ) -> None: |
| if rebase is not None: |
| self.rebase_from = rebase |
| else: |
| self.rebase_from = os.getcwd() |
| self.outputs = [self._rebase(output)] |
| self.deps: set[FilePath] = set() |
| |
| def _rebase(self, path: FilePath) -> FilePath: |
| return os.path.relpath(path, start=self.rebase_from) |
| |
| def add_output(self, output: str | FilePath) -> None: |
| if output not in self.outputs: |
| self.outputs.append(output) |
| |
| def add_input(self, input: FilePath) -> None: |
| """Add an input to the depfile""" |
| self.deps.add(self._rebase(input)) |
| |
| def update(self, other: Union[Self, Iterable[FilePath]]) -> None: |
| """Add each input to this depfile""" |
| # If other is another DepFile, just snag the values from it's internal |
| # dict. |
| inputs: Iterable[FilePath] = set() |
| if isinstance(other, self.__class__): |
| inputs = other.deps |
| elif isinstance(other, Iterable): |
| inputs = other |
| else: |
| raise TypeError( |
| "update() can only accept a DepFile or an iterable of paths" |
| ) |
| |
| # Rebase them all in a bulk operation. |
| inputs = [self._rebase(input) for input in inputs] |
| |
| # And then update the set of deps. |
| for input in inputs: |
| self.deps.add(input) |
| |
| @classmethod |
| def from_deps( |
| cls, |
| output: FilePath, |
| inputs: Iterable[FilePath], |
| rebase: None | FilePath = None, |
| ) -> "DepFile": |
| dep_file = cls(output, rebase) |
| dep_file.update(inputs) |
| return dep_file |
| |
| @classmethod |
| def read_from(cls, file: TextIO) -> "DepFile": |
| depfile = None |
| found_continuation = False |
| |
| for line in file.readlines(): |
| line = line.strip() |
| # ignore empty lines |
| if not line: |
| continue |
| # ignore comment lines |
| if line.startswith("#"): |
| continue |
| # We currently don't allow consecutive backslashes in filenames to |
| # simplify depfile parsing. Support can be added if use cases come up. |
| # |
| # Ninja's implementation: |
| # https://github.com/ninja-build/ninja/blob/5993141c0977f563de5e064fbbe617f9dc34bb8d/src/depfile_parser.cc#L39 |
| if r"\\" in line: |
| raise ValueError( |
| f'Consecutive backslashes found in depfile line "{line}", this is not supported by action tracer' |
| ) |
| |
| # if we haven't parsed out the outputs lines, do that and setup the |
| # depfile object |
| if not depfile: |
| outputs_string, sep, inputs_string = line.partition(":") |
| outputs = shlex.split(outputs_string) |
| inputs = shlex.split(inputs_string) |
| |
| if not sep: |
| raise ValueError( |
| "Fail to parse depfile, no separator found on first line:\n" |
| + line |
| ) |
| if len(outputs) == 0: |
| raise ValueError( |
| "Failed to parse depfile, no outputs found:\n" + line |
| ) |
| |
| depfile = DepFile(outputs[0]) |
| for output in outputs[1:]: |
| depfile.add_output(output) |
| |
| if inputs[-1] in ["\\\n", "\\\r\n"]: |
| found_continuation = True |
| inputs.pop() |
| |
| for input in inputs: |
| depfile.add_input(input) |
| else: |
| if not found_continuation: |
| raise ValueError( |
| "Found non-empty line without preceding continuation marker." |
| + line |
| ) |
| inputs = shlex.split(line) |
| if inputs[-1] in ["\\\n", "\\\r\n"]: |
| found_continuation = True |
| for input in inputs[:-1]: |
| depfile.add_input(input) |
| |
| if depfile: |
| return depfile |
| else: |
| raise ValueError("Depfile was empty") |
| |
| def __repr__(self) -> str: |
| formatted_outputs = " ".join([str(s) for s in self.outputs]) |
| if self.deps: |
| sorted_deps = sorted(str(d) for d in self.deps) |
| input_continuation = " \\\n " |
| return f"{formatted_outputs}: \\\n {input_continuation.join(sorted_deps)}\n" |
| else: |
| return f"{formatted_outputs}:\n" |
| |
| def write_to(self, file: TextIO) -> None: |
| """Write out the depfile contents to the given writeable `file-like` object.""" |
| file.write(str(self)) |