blob: 0e25b7b12f76634646eca0e6bfa64cc509cab23c [file] [log] [blame]
#!/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