| #!/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 |