| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| # | |
| # Copyright (C) 2022 F4PGA Authors | |
| # | |
| # 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. | |
| # | |
| # SPDX-License-Identifier: Apache-2.0 | |
| from pathlib import Path | |
| from colorama import Fore, Style | |
| from f4pga.flows.common import deep, sfprint, bin_dir_path, share_dir_path, F4PGAException | |
| from f4pga.flows.cache import F4Cache | |
| from f4pga.flows.flow_config import FlowConfig | |
| from f4pga.flows.runner import ModRunCtx, module_map, module_exec | |
| from f4pga.flows.stage import Stage | |
| class Flow: | |
| """Describes a complete, configured flow, ready for execution.""" | |
| # Dependendecy to build | |
| target: str | |
| # Values in global scope | |
| cfg: FlowConfig | |
| # dependency-producer map | |
| os_map: "dict[str, Stage]" | |
| # Paths resolved for dependencies | |
| dep_paths: "dict[str, str | list[str]]" | |
| # Explicit configs for dependency paths | |
| # config_paths: 'dict[str, str | list[str]]' | |
| # Stages that need to be run | |
| run_stages: "set[str]" | |
| # Number of stages that relied on outdated version of a (checked) dependency | |
| deps_rebuilds: "dict[str, int]" | |
| f4cache: "F4Cache | None" | |
| flow_cfg: FlowConfig | |
| def __init__(self, target: str, cfg: FlowConfig, f4cache: "F4Cache | None"): | |
| self.target = target | |
| # Associate a stage with every possible output. | |
| # This is commonly refferef to as `os_map` (output-stage-map) through the code. | |
| os_map: "dict[str, Stage]" = {} # Output-Stage map | |
| for stage in cfg.stages.values(): | |
| for output in stage.produces: | |
| if not os_map.get(output.name): | |
| os_map[output.name] = stage | |
| elif os_map[output.name] != stage: | |
| raise Exception( | |
| f"Dependency `{output.name}` is generated by " | |
| f"stage `{os_map[output.name].name}` and " | |
| f"`{stage.name}`. Dependencies can have only one " | |
| "provider at most." | |
| ) | |
| self.os_map = os_map | |
| self.dep_paths = { | |
| n: p | |
| for n, p in cfg.get_dependency_overrides().items() | |
| if (p is not None) and p_req_exists(p) # and not p_dep_differ(p, f4cache) | |
| } | |
| if f4cache is not None: | |
| for dep in self.dep_paths.values(): | |
| self._cache_deps(dep, f4cache) | |
| self.run_stages = set() | |
| self.f4cache = f4cache | |
| self.cfg = cfg | |
| self.deps_rebuilds = {} | |
| self._resolve_dependencies(self.target, set()) | |
| @staticmethod | |
| def _config_mod_runctx( | |
| stage: Stage, | |
| values: "dict[str, ]", | |
| dep_paths: "dict[str, str | list[str]]", | |
| config_paths: "dict[str, str | list[str]]", | |
| ): | |
| takes = {} | |
| for take in stage.takes: | |
| paths = dep_paths.get(take.name) | |
| if paths: # Some takes may be not required | |
| takes[take.name] = paths | |
| produces = {} | |
| for prod in stage.produces: | |
| if dep_paths.get(prod.name): | |
| produces[prod.name] = dep_paths[prod.name] | |
| elif config_paths.get(prod.name): | |
| produces[prod.name] = config_paths[prod.name] | |
| return ModRunCtx(share_dir_path, bin_dir_path, {"takes": takes, "produces": produces, "values": values}) | |
| @staticmethod | |
| def _cache_deps(path: str, f4cache: F4Cache): | |
| def _process_dep_path(path: str, f4cache: F4Cache): | |
| f4cache.process_file(Path(path)) | |
| deep(_process_dep_path)(path, f4cache) | |
| def _dep_will_differ(self, dep: str, paths, consumer: str): | |
| """ | |
| Check if a dependency or any of the dependencies it depends on differ from | |
| their last versions. | |
| """ | |
| if not self.f4cache: # Handle --nocache mode | |
| return True | |
| provider = self.os_map.get(dep) | |
| if provider and (provider.name in self.run_stages): | |
| return True | |
| return p_dep_differ(paths, consumer, self.f4cache) | |
| def _resolve_dependencies(self, dep: str, stages_checked: "set[str]", skip_dep_warnings: "set[str]" = None): | |
| if skip_dep_warnings is None: | |
| skip_dep_warnings = set() | |
| # Initialize the dependency status if necessary | |
| if self.deps_rebuilds.get(dep) is None: | |
| self.deps_rebuilds[dep] = 0 | |
| # Check if an explicit dependency is already resolved | |
| paths = self.dep_paths.get(dep) | |
| if paths and not self.os_map.get(dep): | |
| return | |
| # Check if a stage can provide the required dependency | |
| provider = self.os_map.get(dep) | |
| if not provider or provider.name in stages_checked: | |
| return | |
| # TODO: Check if the dependency is "on-demand" and force it in provider's | |
| # config if it is. | |
| for take in provider.takes: | |
| self._resolve_dependencies(take.name, stages_checked, skip_dep_warnings) | |
| # If any of the required dependencies is unavailable, then the | |
| # provider stage cannot be run | |
| take_paths = self.dep_paths.get(take.name) | |
| # Add input path to values (dirty hack) | |
| provider.value_overrides[f":{take.name}"] = take_paths | |
| if not take_paths and take.spec == "req": | |
| sfprint( | |
| 0, | |
| f" Stage `{Style.BRIGHT + provider.name + Style.RESET_ALL}` is " | |
| f"unreachable due to unmet dependency `{Style.BRIGHT + take.name + Style.RESET_ALL}`", | |
| ) | |
| return | |
| will_differ = False | |
| if take_paths is None: | |
| # TODO: This won't trigger rebuild if an optional dependency got removed | |
| will_differ = False | |
| elif p_req_exists(take_paths): | |
| will_differ = self._dep_will_differ(take.name, take_paths, provider.name) | |
| else: | |
| will_differ = True | |
| if will_differ: | |
| if take.name not in skip_dep_warnings: | |
| sfprint( | |
| 2, | |
| f"{Style.BRIGHT}{take.name}{Style.RESET_ALL} is causing " | |
| f"rebuild for `{Style.BRIGHT}{provider.name}{Style.RESET_ALL}`", | |
| ) | |
| skip_dep_warnings.add(take.name) | |
| self.run_stages.add(provider.name) | |
| self.deps_rebuilds[take.name] += 1 | |
| outputs = module_map( | |
| provider.module, | |
| self._config_mod_runctx( | |
| provider, self.cfg.get_r_env(provider.name).values, self.dep_paths, self.cfg.get_dependency_overrides() | |
| ), | |
| ) | |
| for output_paths in outputs.values(): | |
| if output_paths is not None: | |
| if p_req_exists(output_paths) and self.f4cache: | |
| self._cache_deps(output_paths, self.f4cache) | |
| stages_checked.add(provider.name) | |
| self.dep_paths.update(outputs) | |
| for _, out_paths in outputs.items(): | |
| if (out_paths is not None) and not (p_req_exists(out_paths)): | |
| self.run_stages.add(provider.name) | |
| # Verify module's outputs and add paths as values. | |
| outs = outputs.keys() | |
| for o in provider.produces: | |
| if o.name not in outs: | |
| if o.spec == "req" or (o.spec == "demand" and o.name in self.cfg.get_dependency_overrides().keys()): | |
| fatal(-1, f"Module {provider.name} did not produce a mapping " f"for a required output `{o.name}`") | |
| else: | |
| # Remove an on-demand/optional output that is not produced | |
| # from os_map. | |
| self.os_map.pop(o.name) | |
| # Add a value for the output (dirty ack yet again) | |
| o_path = outputs.get(o.name) | |
| if o_path is not None: | |
| provider.value_overrides[f":{o.name}"] = outputs.get(o.name) | |
| def print_resolved_dependencies(self, verbosity: int): | |
| deps = list(self.deps_rebuilds.keys()) | |
| deps.sort() | |
| for dep in deps: | |
| status = Fore.RED + "[X]" + Fore.RESET | |
| source = Fore.YELLOW + "MISSING" + Fore.RESET | |
| paths = self.dep_paths.get(dep) | |
| if paths: | |
| exists = p_req_exists(paths) | |
| provider = self.os_map.get(dep) | |
| if provider and provider.name in self.run_stages: | |
| status = Fore.YELLOW + ("[R]" if exists else "[S]") + Fore.RESET | |
| source = f"{Fore.BLUE + self.os_map[dep].name + Fore.RESET} -> {paths}" | |
| elif exists: | |
| status = Fore.GREEN + ("[N]" if self.deps_rebuilds[dep] > 0 else "[O]") + Fore.RESET | |
| source = paths | |
| elif self.os_map.get(dep): | |
| status = Fore.RED + "[U]" + Fore.RESET | |
| source = f"{Fore.BLUE + self.os_map[dep].name + Fore.RESET} -> ???" | |
| sfprint(verbosity, f" {Style.BRIGHT + status} " f"{dep + Style.RESET_ALL}: {source}") | |
| def _build_dep(self, dep): | |
| provider = self.os_map.get(dep) | |
| r_env = self.cfg.r_env if provider is None else self.cfg.get_r_env(provider.name) | |
| paths = r_env.resolve(self.dep_paths.get(dep)) | |
| if not paths: | |
| sfprint(2, f"Dependency {dep} is unresolved.") | |
| return False | |
| run = (provider.name in self.run_stages) if provider else False | |
| if p_req_exists(paths) and not run: | |
| return True | |
| else: | |
| assert provider | |
| any_dep_differ = False if (self.f4cache is not None) else True | |
| for p_dep in provider.takes: | |
| if not self._build_dep(p_dep.name): | |
| assert p_dep.spec != "req" | |
| continue | |
| if self.f4cache is not None: | |
| any_dep_differ |= p_update_dep_statuses(self.dep_paths[p_dep.name], provider.name, self.f4cache) | |
| # If dependencies remained the same, consider the dep as up-to date | |
| # For example, when changing a comment in Verilog source code, | |
| # the initial dependency resolution will report a need for complete | |
| # rebuild, however, after the synthesis stage, the generated eblif | |
| # will reamin the same, thus making it unnecessary to continue the | |
| # rebuild process. | |
| if (not any_dep_differ) and p_req_exists(paths): | |
| sfprint( | |
| 2, | |
| f"Skipping rebuild of `" | |
| f"{Style.BRIGHT + dep + Style.RESET_ALL}` because all " | |
| f"of it's dependencies remained unchanged", | |
| ) | |
| return True | |
| module_exec( | |
| provider.module, | |
| self._config_mod_runctx( | |
| provider, | |
| self.cfg.get_r_env(provider.name).values, | |
| self.dep_paths, | |
| self.cfg.get_dependency_overrides(), | |
| ), | |
| ) | |
| self.run_stages.discard(provider.name) | |
| for product in provider.produces: | |
| if (product.spec == "req") and not p_req_exists(paths): | |
| raise DependencyNotProducedException(dep, provider.name) | |
| prod_paths = self.dep_paths[product.name] | |
| if (prod_paths is not None) and p_req_exists(paths) and self.f4cache: | |
| self._cache_deps(prod_paths, self.f4cache) | |
| return True | |
| def execute(self): | |
| self._build_dep(self.target) | |
| if self.f4cache: | |
| self._cache_deps(self.dep_paths[self.target], self.f4cache) | |
| p_update_dep_statuses(self.dep_paths[self.target], "__target", self.f4cache) | |
| sfprint(0, f"Target {Style.BRIGHT + self.target + Style.RESET_ALL} -> {self.dep_paths[self.target]}") | |
| class DependencyNotProducedException(F4PGAException): | |
| dep_name: str | |
| provider: str | |
| def __init__(self, dep_name: str, provider: str): | |
| self.dep_name = dep_name | |
| self.provider = provider | |
| self.message = f"Stage `{self.provider}` did not produce promised dependency `{self.dep_name}`" | |
| def p_req_exists(r): | |
| """ | |
| Checks whether a dependency exists on a drive. | |
| """ | |
| if type(r) is str: | |
| if not Path(r).exists(): | |
| return False | |
| elif type(r) is list: | |
| return not (False in map(p_req_exists, r)) | |
| else: | |
| raise Exception(f"Requirements can be currently checked only for single paths, or path lists (reason: {r})") | |
| return True | |
| def p_update_dep_statuses(paths, consumer: str, f4cache: F4Cache): | |
| if type(paths) is str: | |
| return f4cache.update(Path(paths), consumer) | |
| elif type(paths) is list: | |
| for p in paths: | |
| return p_update_dep_statuses(p, consumer, f4cache) | |
| elif type(paths) is dict: | |
| for _, p in paths.items(): | |
| return p_update_dep_statuses(p, consumer, f4cache) | |
| fatal(-1, "WRONG PATHS TYPE") | |
| def p_dep_differ(paths, consumer: str, f4cache: F4Cache): | |
| """ | |
| Check if a dependency differs from its last version, lack of dependency is treated as "differs". | |
| """ | |
| if type(paths) is str: | |
| if not Path(paths).exists(): | |
| return True | |
| return f4cache.get_status(paths, consumer) != "same" | |
| elif type(paths) is list: | |
| return True in [p_dep_differ(p, consumer, f4cache) for p in paths] | |
| elif type(paths) is dict: | |
| return True in [p_dep_differ(p, consumer, f4cache) for _, p in paths.items()] | |
| return False |