| #!/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 |
| |
| """ |
| Here are the things necessary to write an F4PGA Module. |
| """ |
| |
| from types import SimpleNamespace |
| from abc import abstractmethod |
| |
| from f4pga.flows.common import decompose_depname, ResolutionEnv, fatal |
| |
| |
| class Module: |
| """ |
| A `Module` is a wrapper for whatever tool is used in a flow. |
| Modules can request dependencies, values and are guranteed to have all the required ones present when entering |
| `exec` mode. |
| They also have to specify what dependencies they produce and create the files for these dependencies. |
| """ |
| |
| no_of_phases: int |
| name: str |
| takes: "list[str]" |
| produces: "list[str]" |
| values: "list[str]" |
| prod_meta: "dict[str, str]" |
| |
| @abstractmethod |
| def execute(self, ctx): |
| """ |
| Executes module. |
| Use yield to print a message informing about current execution phase. |
| `ctx` is `ModuleContext`. |
| """ |
| pass |
| |
| @abstractmethod |
| def map_io(self, ctx) -> "dict[str, ]": |
| """ |
| Returns paths for outputs derived from given inputs. |
| `ctx` is `ModuleContext`. |
| """ |
| pass |
| |
| def __init__(self, params: "dict[str, ]"): |
| self.no_of_phases = 0 |
| self.current_phase = 0 |
| self.name = "<BASE STAGE>" |
| self.prod_meta = {} |
| |
| |
| class ModuleContext: |
| """ |
| A class for object holding mappings for dependencies and values as well as other information needed during modules |
| execution. |
| """ |
| |
| share: str # Absolute path to F4PGA's share directory |
| bin: str # Absolute path to F4PGA's bin directory |
| takes: SimpleNamespace # Maps symbolic dependency names to relative paths. |
| produces: SimpleNamespace # Contains mappings for explicitely specified dependencies. |
| # Useful mostly for checking for on-demand optional outputs (such as logs) with |
| # `is_output_explicit` method. |
| outputs: SimpleNamespace # Contains mappings for all available outputs. |
| values: SimpleNamespace # Contains all available requested values. |
| r_env: ResolutionEnv # `ResolutionEnvironmet` object holding mappings for current scope. |
| module_name: str # Name of the module. |
| |
| def is_output_explicit(self, name: str): |
| """ |
| True if user has explicitely specified output's path. |
| """ |
| return getattr(self.produces, name) is not None |
| |
| def _getreqmaybe(self, obj, deps: "list[str]", deps_cfg: "dict[str, ]"): |
| """ |
| Add attribute for a dependency or panic if a required dependency has not been given to the module on its input. |
| """ |
| for name in deps: |
| name, spec = decompose_depname(name) |
| value = deps_cfg.get(name) |
| if value is None and spec == "req": |
| fatal(-1, f"Dependency `{name}` is required by module `{self.module_name}` but wasn't provided") |
| setattr(obj, name, self.r_env.resolve(value)) |
| |
| # `config` should be a dictionary given as modules input. |
| def __init__(self, module: Module, config: "dict[str, ]", r_env: ResolutionEnv, share: str, bin: str): |
| self.module_name = module.name |
| self.takes = SimpleNamespace() |
| self.produces = SimpleNamespace() |
| self.values = SimpleNamespace() |
| self.outputs = SimpleNamespace() |
| self.r_env = r_env |
| self.share = share |
| self.bin = bin |
| |
| self._getreqmaybe(self.takes, module.takes, config["takes"]) |
| self._getreqmaybe(self.values, module.values, config["values"]) |
| |
| produces_resolved = self.r_env.resolve(config["produces"]) |
| for name, value in produces_resolved.items(): |
| setattr(self.produces, name, value) |
| |
| outputs = module.map_io(self) |
| outputs.update(produces_resolved) |
| |
| self._getreqmaybe(self.outputs, module.produces, outputs) |
| |
| def shallow_copy(self): |
| cls = type(self) |
| mycopy = cls.__new__(cls) |
| |
| mycopy.module_name = self.module_name |
| mycopy.takes = self.takes |
| mycopy.produces = self.produces |
| mycopy.values = self.values |
| mycopy.outputs = self.outputs |
| mycopy.r_env = self.r_env |
| mycopy.share = self.share |
| mycopy.bin = self.bin |
| |
| return mycopy |
| |
| |
| class ModuleRuntimeException(Exception): |
| info: str |
| |
| def __init__(self, info: str): |
| self.info = info |
| |
| def __str___(self): |
| return self.info |
| |
| |
| def get_mod_metadata(module: Module): |
| """ |
| Get descriptions for produced dependencies. |
| """ |
| meta = {} |
| has_meta = hasattr(module, "prod_meta") |
| for prod in module.produces: |
| prod = prod.replace("?", "").replace("!", "") |
| if not has_meta: |
| meta[prod] = "<no descritption>" |
| continue |
| prod_meta = module.prod_meta.get(prod) |
| meta[prod] = prod_meta if prod_meta else "<no description>" |
| return meta |