f4pga: add submodule 'flows'

Signed-off-by: Unai Martinez-Corral <umartinezcorral@antmicro.com>
This commit is contained in:
Unai Martinez-Corral 2022-08-18 05:12:21 +02:00
parent 8eb0441d71
commit b6f8fe83dc
29 changed files with 857 additions and 807 deletions

782
f4pga/__init__.py Executable file → Normal file
View File

@ -1,758 +1,24 @@
#!/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
"""
F4PGA Build System
This tool allows for building FPGA targets (such as bitstreams) for any supported platform with just one simple command
and a project file.
The idea is that F4PGA wraps all the tools needed by different platforms in "modules", which define inputs/outputs and
various parameters.
This allows F4PGA to resolve dependencies for any target provided that a "flow definition" file exists for such target.
The flow defeinition file list modules available for that platform and may tweak some settings of those modules.
A basic example of using F4PGA:
$ f4pga build --flow flow.json --part XC7A35TCSG324-1 -t bitstream
This will make F4PGA attempt to create a bitstream for arty_35 platform.
``flow.json`` is a flow configuration file, which should be created for a project that uses F4PGA.
Contains project-specific definitions needed within the flow, such as list of source code files.
"""
from pathlib import Path
from argparse import Namespace
from sys import argv as sys_argv
from os import environ
from yaml import load as yaml_load, Loader as yaml_loader
from typing import Iterable
from colorama import Fore, Style
from f4pga.common import (
F4PGAException,
ResolutionEnv,
deep,
fatal,
scan_modules,
set_verbosity_level,
sfprint,
sub as common_sub
)
from f4pga.cache import F4Cache
from f4pga.flow_config import (
ProjectFlowConfig,
FlowConfig,
FlowDefinition,
open_project_flow_cfg,
verify_platform_name
)
from f4pga.module_runner import ModRunCtx, module_map, module_exec
from f4pga.module_inspector import get_module_info
from f4pga.stage import Stage
from f4pga.argparser import setup_argparser, get_cli_flow_config
F4CACHEPATH = '.f4cache'
install_dir = environ.get("F4PGA_INSTALL_DIR", "/usr/local")
ROOT = Path(__file__).resolve().parent
FPGA_FAM = environ.get('FPGA_FAM', 'xc7')
if FPGA_FAM not in ['xc7', 'eos-s3', 'qlf_k4n8']:
raise(Exception(f"Unsupported FPGA_FAM <{FPGA_FAM}>!"))
bin_dir_path = str(Path(sys_argv[0]).resolve().parent.parent)
share_dir_path = \
environ.get('F4PGA_SHARE_DIR',
str(Path(f'{install_dir}/{FPGA_FAM}/share/f4pga').resolve()))
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 ' \
f'dependency `{self.dep_name}`'
def dep_value_str(dep: str):
return ':' + dep
def platform_stages(platform_flow, r_env):
""" Iterates over all stages available in a given flow. """
stage_options = platform_flow.get('stage_options')
for stage_name, modulestr in platform_flow['stages'].items():
mod_opts = stage_options.get(stage_name) if stage_options else None
yield Stage(stage_name, modulestr, mod_opts, r_env)
def 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(req_exists, r))
else:
raise Exception(f'Requirements can be currently checked only for single '
f'paths, or path lists (reason: {r})')
return True
def map_outputs_to_stages(stages: 'list[Stage]'):
"""
Associates 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 stages:
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.')
return os_map
def filter_existing_deps(deps: 'dict[str, ]', f4cache):
return [(n, p) for n, p in deps.items() \
if req_exists(p)] # and not dep_differ(p, f4cache)]
def get_stage_values_override(og_values: dict, stage: Stage):
values = og_values.copy()
values.update(stage.value_ovds)
return values
def prepare_stage_io_input(stage: Stage):
return { 'params': stage.params } if stage.params is not None else {}
def prepare_stage_input(stage: Stage, values: dict, dep_paths: 'dict[str, ]',
config_paths: 'dict[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]
stage_mod_cfg = {
'takes': takes,
'produces': produces,
'values': values
}
return stage_mod_cfg
def 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 update_dep_statuses(p, consumer, f4cache)
elif type(paths) is dict:
for _, p in paths.items():
return update_dep_statuses(p, consumer, f4cache)
fatal(-1, 'WRONG PATHS TYPE')
def 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 [dep_differ(p, consumer, f4cache) for p in paths]
elif type(paths) is dict:
return True in [dep_differ(p, consumer, f4cache) \
for _, p in paths.items()]
return False
def dep_will_differ(target: str, paths, consumer: str,
os_map: 'dict[str, Stage]', run_stages: 'set[str]',
f4cache: F4Cache):
"""
Check if a dependency or any of the dependencies it depends on differ from
their last versions.
"""
provider = os_map.get(target)
if provider:
return (provider.name in run_stages) or \
dep_differ(paths, consumer, f4cache)
return dep_differ(paths, consumer, f4cache)
def _print_unreachable_stage_message(provider: Stage, take: str):
sfprint(0, ' Stage '
f'`{Style.BRIGHT + provider.name + Style.RESET_ALL}` is '
'unreachable due to unmet dependency '
f'`{Style.BRIGHT + take.name + Style.RESET_ALL}`')
def config_mod_runctx(stage: Stage, values: 'dict[str, ]',
dep_paths: 'dict[str, str | list[str]]',
config_paths: 'dict[str, str | list[str]]'):
config = prepare_stage_input(stage, values,
dep_paths, config_paths)
return ModRunCtx(share_dir_path, bin_dir_path, config)
def _process_dep_path(path: str, f4cache: F4Cache):
f4cache.process_file(Path(path))
_cache_deps = deep(_process_dep_path)
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
self.os_map = map_outputs_to_stages(cfg.stages.values())
explicit_deps = cfg.get_dependency_overrides()
self.dep_paths = dict(filter_existing_deps(explicit_deps, f4cache))
if f4cache is not None:
for dep in self.dep_paths.values():
_cache_deps(dep, f4cache)
self.run_stages = set()
self.f4cache = f4cache
self.cfg = cfg
self.deps_rebuilds = {}
self._resolve_dependencies(self.target, set())
def _dep_will_differ(self, dep: str, paths, consumer: str):
if not self.f4cache: # Handle --nocache mode
return True
return dep_will_differ(dep, paths, consumer,
self.os_map, self.run_stages,
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[dep_value_str(take.name)] = take_paths
if not take_paths and take.spec == 'req':
_print_unreachable_stage_message(provider, take)
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 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
stage_values = self.cfg.get_r_env(provider.name).values
modrunctx = config_mod_runctx(provider, stage_values, self.dep_paths,
self.cfg.get_dependency_overrides())
outputs = module_map(provider.module, modrunctx)
for output_paths in outputs.values():
if output_paths is not None:
if req_exists(output_paths) and self.f4cache:
_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 (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[dep_value_str(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 = req_exists(paths)
provider = self.os_map.get(dep)
if provider and provider.name in self.run_stages:
if exists:
status = Fore.YELLOW + '[R]' + Fore.RESET
else:
status = Fore.YELLOW + '[S]' + Fore.RESET
source = f'{Fore.BLUE + self.os_map[dep].name + Fore.RESET} ' \
f'-> {paths}'
elif exists:
if self.deps_rebuilds[dep] > 0:
status = Fore.GREEN + '[N]' + Fore.RESET
else:
status = Fore.GREEN + '[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):
paths = self.dep_paths.get(dep)
provider = self.os_map.get(dep)
run = (provider.name in self.run_stages) if provider else False
if not paths:
sfprint(2, f'Dependency {dep} is unresolved.')
return False
if 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 |= \
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 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
stage_values = self.cfg.get_r_env(provider.name).values
modrunctx = config_mod_runctx(provider, stage_values, self.dep_paths,
self.cfg.get_dependency_overrides())
module_exec(provider.module, modrunctx)
self.run_stages.discard(provider.name)
for product in provider.produces:
if (product.spec == 'req') and not req_exists(paths):
raise DependencyNotProducedException(dep, provider.name)
prod_paths = self.dep_paths[product.name]
if (prod_paths is not None) and req_exists(paths) and self.f4cache:
_cache_deps(prod_paths, self.f4cache)
return True
def execute(self):
self._build_dep(self.target)
if self.f4cache:
_cache_deps(self.dep_paths[self.target], self.f4cache)
update_dep_statuses(self.dep_paths[self.target], '__target',
self.f4cache)
sfprint(0, f'Target {Style.BRIGHT + self.target + Style.RESET_ALL} '
f'-> {self.dep_paths[self.target]}')
def display_dep_info(stages: 'Iterable[Stage]'):
sfprint(0, 'Platform dependencies/targets:')
longest_out_name_len = 0
for stage in stages:
for out in stage.produces:
l = len(out.name)
if l > longest_out_name_len:
longest_out_name_len = l
desc_indent = longest_out_name_len + 7
nl_indentstr = '\n'
for _ in range(0, desc_indent):
nl_indentstr += ' '
for stage in stages:
for out in stage.produces:
pname = Style.BRIGHT + out.name + Style.RESET_ALL
indent = ''
for _ in range(0, desc_indent - len(pname) + 3):
indent += ' '
specstr = '???'
if out.spec == 'req':
specstr = f'{Fore.BLUE}guaranteed{Fore.RESET}'
elif out.spec == 'maybe':
specstr = f'{Fore.YELLOW}not guaranteed{Fore.RESET}'
elif out.spec == 'demand':
specstr = f'{Fore.RED}on-demand{Fore.RESET}'
pgen = f'{Style.DIM}stage: `{stage.name}`, '\
f'spec: {specstr}{Style.RESET_ALL}'
pdesc = stage.meta[out.name].replace('\n', nl_indentstr)
sfprint(0, f' {Style.BRIGHT + out.name + Style.RESET_ALL}:'
f'{indent}{pdesc}{nl_indentstr}{pgen}')
def display_stage_info(stage: Stage):
if stage is None:
sfprint(0, f'Stage does not exist')
f4pga_fail()
return
sfprint(0, f'Stage `{Style.BRIGHT}{stage.name}{Style.RESET_ALL}`:')
sfprint(0, f' Module: `{Style.BRIGHT}{stage.module.name}{Style.RESET_ALL}`')
sfprint(0, f' Module info:')
mod_info = get_module_info(stage.module)
mod_info = '\n '.join(mod_info.split('\n'))
sfprint(0, f' {mod_info}')
f4pga_done_str = Style.BRIGHT + Fore.GREEN + 'DONE'
def f4pga_fail():
global f4pga_done_str
f4pga_done_str = Style.BRIGHT + Fore.RED + 'FAILED'
def f4pga_done():
sfprint(1, f'f4pga: {f4pga_done_str}'
f'{Style.RESET_ALL + Fore.RESET}')
exit(0)
def setup_resolution_env():
""" Sets up a ResolutionEnv with default built-ins. """
r_env = ResolutionEnv({
'shareDir': share_dir_path,
'binDir': bin_dir_path
})
def _noisy_warnings():
"""
Emit some noisy warnings.
"""
environ['OUR_NOISY_WARNINGS'] = 'noisy_warnings.log'
return 'noisy_warnings.log'
def _generate_values():
"""
Generate initial values, available in configs.
"""
conf = {
'python3': common_sub('which', 'python3').decode().replace('\n', ''),
'noisyWarnings': _noisy_warnings()
}
if (FPGA_FAM == 'xc7'):
conf['prjxray_db'] = common_sub('prjxray-config').decode().replace('\n', '')
return conf
r_env.add_values(_generate_values())
return r_env
def open_project_flow_config(path: str) -> ProjectFlowConfig:
try:
flow_cfg = open_project_flow_cfg(path)
except FileNotFoundError as _:
fatal(-1, 'The provided flow configuration file does not exist')
return flow_cfg
def verify_part_stage_params(flow_cfg: FlowConfig,
part: 'str | None' = None):
if part:
platform_name = get_platform_name_for_part(part)
if not verify_platform_name(platform_name, str(ROOT)):
sfprint(0, f'Platform `{part}`` is unsupported.')
return False
if part not in flow_cfg.part():
sfprint(0, f'Platform `{part}`` is not in project.')
return False
return True
def get_platform_name_for_part(part_name: str):
"""
Gets a name that identifies the platform setup required for a specific chip.
The reason for such distinction is that plenty of chips with different names
differ only in a type of package they use.
"""
with (ROOT / 'part_db.yml').open('r') as rfptr:
for key, val in yaml_load(rfptr, yaml_loader).items():
if part_name.upper() in val:
return key
raise Exception(f"Unknown part name <{part_name}>!")
def make_flow_config(project_flow_cfg: ProjectFlowConfig, part_name: str) -> FlowConfig:
""" Create `FlowConfig` from given project flow configuration and part name """
platform = get_platform_name_for_part(part_name)
if platform is None:
raise F4PGAException(
message='You have to specify a part name or configure a default part.'
)
if part_name not in project_flow_cfg.parts():
raise F4PGAException(
message='Project flow configuration does not support requested part.'
)
r_env = setup_resolution_env()
r_env.add_values({'part_name': part_name.lower()})
scan_modules(str(ROOT))
with (ROOT / 'platforms.yml').open('r') as rfptr:
platforms = yaml_load(rfptr, yaml_loader)
if platform not in platforms:
raise F4PGAException(message=f'Flow definition for platform <{platform}> cannot be found!')
flow_cfg = FlowConfig(
project_flow_cfg,
FlowDefinition(platforms[platform], r_env),
part_name
)
if len(flow_cfg.stages) == 0:
raise F4PGAException(message = 'Platform flow does not define any stage')
return flow_cfg
def cmd_build(args: Namespace):
""" `build` command implementation """
project_flow_cfg: ProjectFlowConfig = None
part_name = args.part
if args.flow:
project_flow_cfg = open_project_flow_config(args.flow)
elif part_name is not None:
project_flow_cfg = ProjectFlowConfig('.temp.flow.json')
project_flow_cfg.flow_cfg = get_cli_flow_config(args, part_name)
if part_name is None and project_flow_cfg is not None:
part_name = project_flow_cfg.get_default_part()
if project_flow_cfg is None:
fatal(-1, 'No configuration was provided. Use `--flow`, and/or '
'`--part` to configure flow.')
flow_cfg = make_flow_config(project_flow_cfg, part_name)
if args.info:
display_dep_info(flow_cfg.stages.values())
f4pga_done()
if args.stageinfo:
display_stage_info(flow_cfg.stages.get(args.stageinfo[0]))
f4pga_done()
target = args.target
if target is None:
target = project_flow_cfg.get_default_target(part_name)
if target is None:
fatal(-1, 'Please specify desired target using `--target` option '
'or configure a default target.')
flow = Flow(
target=target,
cfg=flow_cfg,
f4cache=F4Cache(F4CACHEPATH) if not args.nocache else None
)
dep_print_verbosity = 0 if args.pretend else 2
sfprint(dep_print_verbosity, '\nProject status:')
flow.print_resolved_dependencies(dep_print_verbosity)
sfprint(dep_print_verbosity, '')
if args.pretend:
f4pga_done()
try:
flow.execute()
except AssertionError as e:
raise e
except Exception as e:
sfprint(0, f'{e}')
f4pga_fail()
if flow.f4cache:
flow.f4cache.save()
def cmd_show_dependencies(args: Namespace):
""" `showd` command implementation """
flow_cfg = open_project_flow_config(args.flow)
if not verify_part_stage_params(flow_cfg, args.part):
f4pga_fail()
return
platform_overrides: 'set | None' = None
if args.platform is not None:
platform_overrides = \
set(flow_cfg.get_dependency_platform_overrides(args.part).keys())
display_list = []
raw_deps = flow_cfg.get_dependencies_raw(args.platform)
for dep_name, dep_paths in raw_deps.items():
prstr: str
if (platform_overrides is not None) and (dep_name in platform_overrides):
prstr = f'{Style.DIM}({args.platform}){Style.RESET_ALL} ' \
f'{Style.BRIGHT + dep_name + Style.RESET_ALL}: {dep_paths}'
else:
prstr = f'{Style.BRIGHT + dep_name + Style.RESET_ALL}: {dep_paths}'
display_list.append((dep_name, prstr))
display_list.sort(key = lambda p: p[0])
for _, prstr in display_list:
sfprint(0, prstr)
set_verbosity_level(-1)
def main():
parser = setup_argparser()
args = parser.parse_args()
set_verbosity_level(args.verbose - (1 if args.silent else 0))
if args.command == 'build':
cmd_build(args)
f4pga_done()
if args.command == 'showd':
cmd_show_dependencies(args)
f4pga_done()
sfprint(0, 'Please use a command.\nUse `--help` flag to learn more.')
f4pga_done()
if __name__ == '__main__':
main()
#!/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 f4pga.flows import main
if __name__ == '__main__':
main()

31
f4pga/context.py Normal file
View File

@ -0,0 +1,31 @@
#!/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 os import environ
FPGA_FAM = environ.get('FPGA_FAM', 'xc7')
if FPGA_FAM not in ['xc7', 'eos-s3', 'qlf_k4n8']:
raise(Exception(f"Unsupported FPGA_FAM <{FPGA_FAM}>!"))
F4PGA_INSTALL_DIR = environ.get('F4PGA_INSTALL_DIR')
if F4PGA_INSTALL_DIR is None:
raise(Exception("Required environment variable F4PGA_INSTALL_DIR is undefined!"))
F4PGA_INSTALL_DIR_PATH = Path(F4PGA_INSTALL_DIR)

755
f4pga/flows/__init__.py Executable file
View File

@ -0,0 +1,755 @@
#!/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
"""
F4PGA Build System
This tool allows for building FPGA targets (such as bitstreams) for any supported platform with just one simple command
and a project file.
The idea is that F4PGA wraps all the tools needed by different platforms in "modules", which define inputs/outputs and
various parameters.
This allows F4PGA to resolve dependencies for any target provided that a "flow definition" file exists for such target.
The flow defeinition file list modules available for that platform and may tweak some settings of those modules.
A basic example of using F4PGA:
$ f4pga build --flow flow.json --part XC7A35TCSG324-1 -t bitstream
This will make F4PGA attempt to create a bitstream for arty_35 platform.
``flow.json`` is a flow configuration file, which should be created for a project that uses F4PGA.
Contains project-specific definitions needed within the flow, such as list of source code files.
"""
from pathlib import Path
from argparse import Namespace
from sys import argv as sys_argv
from os import environ
from yaml import load as yaml_load, Loader as yaml_loader
from typing import Iterable
from colorama import Fore, Style
from f4pga.context import FPGA_FAM
from f4pga.flows.common import (
F4PGAException,
ResolutionEnv,
deep,
fatal,
scan_modules,
set_verbosity_level,
sfprint,
sub as common_sub
)
from f4pga.flows.cache import F4Cache
from f4pga.flows.flow_config import (
ProjectFlowConfig,
FlowConfig,
FlowDefinition,
open_project_flow_cfg,
verify_platform_name
)
from f4pga.flows.module_runner import ModRunCtx, module_map, module_exec
from f4pga.flows.module_inspector import get_module_info
from f4pga.flows.stage import Stage
from f4pga.flows.argparser import setup_argparser, get_cli_flow_config
F4CACHEPATH = '.f4cache'
install_dir = environ.get("F4PGA_INSTALL_DIR", "/usr/local")
ROOT = Path(__file__).resolve().parent
bin_dir_path = str(Path(sys_argv[0]).resolve().parent.parent)
share_dir_path = \
environ.get('F4PGA_SHARE_DIR',
str(Path(f'{install_dir}/{FPGA_FAM}/share/f4pga').resolve()))
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 ' \
f'dependency `{self.dep_name}`'
def dep_value_str(dep: str):
return ':' + dep
def platform_stages(platform_flow, r_env):
""" Iterates over all stages available in a given flow. """
stage_options = platform_flow.get('stage_options')
for stage_name, modulestr in platform_flow['stages'].items():
mod_opts = stage_options.get(stage_name) if stage_options else None
yield Stage(stage_name, modulestr, mod_opts, r_env)
def 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(req_exists, r))
else:
raise Exception(f'Requirements can be currently checked only for single '
f'paths, or path lists (reason: {r})')
return True
def map_outputs_to_stages(stages: 'list[Stage]'):
"""
Associates 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 stages:
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.')
return os_map
def filter_existing_deps(deps: 'dict[str, ]', f4cache):
return [(n, p) for n, p in deps.items() \
if req_exists(p)] # and not dep_differ(p, f4cache)]
def get_stage_values_override(og_values: dict, stage: Stage):
values = og_values.copy()
values.update(stage.value_ovds)
return values
def prepare_stage_io_input(stage: Stage):
return { 'params': stage.params } if stage.params is not None else {}
def prepare_stage_input(stage: Stage, values: dict, dep_paths: 'dict[str, ]',
config_paths: 'dict[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]
stage_mod_cfg = {
'takes': takes,
'produces': produces,
'values': values
}
return stage_mod_cfg
def 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 update_dep_statuses(p, consumer, f4cache)
elif type(paths) is dict:
for _, p in paths.items():
return update_dep_statuses(p, consumer, f4cache)
fatal(-1, 'WRONG PATHS TYPE')
def 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 [dep_differ(p, consumer, f4cache) for p in paths]
elif type(paths) is dict:
return True in [dep_differ(p, consumer, f4cache) \
for _, p in paths.items()]
return False
def dep_will_differ(target: str, paths, consumer: str,
os_map: 'dict[str, Stage]', run_stages: 'set[str]',
f4cache: F4Cache):
"""
Check if a dependency or any of the dependencies it depends on differ from
their last versions.
"""
provider = os_map.get(target)
if provider:
return (provider.name in run_stages) or \
dep_differ(paths, consumer, f4cache)
return dep_differ(paths, consumer, f4cache)
def _print_unreachable_stage_message(provider: Stage, take: str):
sfprint(0, ' Stage '
f'`{Style.BRIGHT + provider.name + Style.RESET_ALL}` is '
'unreachable due to unmet dependency '
f'`{Style.BRIGHT + take.name + Style.RESET_ALL}`')
def config_mod_runctx(stage: Stage, values: 'dict[str, ]',
dep_paths: 'dict[str, str | list[str]]',
config_paths: 'dict[str, str | list[str]]'):
config = prepare_stage_input(stage, values,
dep_paths, config_paths)
return ModRunCtx(share_dir_path, bin_dir_path, config)
def _process_dep_path(path: str, f4cache: F4Cache):
f4cache.process_file(Path(path))
_cache_deps = deep(_process_dep_path)
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
self.os_map = map_outputs_to_stages(cfg.stages.values())
explicit_deps = cfg.get_dependency_overrides()
self.dep_paths = dict(filter_existing_deps(explicit_deps, f4cache))
if f4cache is not None:
for dep in self.dep_paths.values():
_cache_deps(dep, f4cache)
self.run_stages = set()
self.f4cache = f4cache
self.cfg = cfg
self.deps_rebuilds = {}
self._resolve_dependencies(self.target, set())
def _dep_will_differ(self, dep: str, paths, consumer: str):
if not self.f4cache: # Handle --nocache mode
return True
return dep_will_differ(dep, paths, consumer,
self.os_map, self.run_stages,
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[dep_value_str(take.name)] = take_paths
if not take_paths and take.spec == 'req':
_print_unreachable_stage_message(provider, take)
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 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
stage_values = self.cfg.get_r_env(provider.name).values
modrunctx = config_mod_runctx(provider, stage_values, self.dep_paths,
self.cfg.get_dependency_overrides())
outputs = module_map(provider.module, modrunctx)
for output_paths in outputs.values():
if output_paths is not None:
if req_exists(output_paths) and self.f4cache:
_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 (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[dep_value_str(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 = req_exists(paths)
provider = self.os_map.get(dep)
if provider and provider.name in self.run_stages:
if exists:
status = Fore.YELLOW + '[R]' + Fore.RESET
else:
status = Fore.YELLOW + '[S]' + Fore.RESET
source = f'{Fore.BLUE + self.os_map[dep].name + Fore.RESET} ' \
f'-> {paths}'
elif exists:
if self.deps_rebuilds[dep] > 0:
status = Fore.GREEN + '[N]' + Fore.RESET
else:
status = Fore.GREEN + '[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):
paths = self.dep_paths.get(dep)
provider = self.os_map.get(dep)
run = (provider.name in self.run_stages) if provider else False
if not paths:
sfprint(2, f'Dependency {dep} is unresolved.')
return False
if 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 |= \
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 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
stage_values = self.cfg.get_r_env(provider.name).values
modrunctx = config_mod_runctx(provider, stage_values, self.dep_paths,
self.cfg.get_dependency_overrides())
module_exec(provider.module, modrunctx)
self.run_stages.discard(provider.name)
for product in provider.produces:
if (product.spec == 'req') and not req_exists(paths):
raise DependencyNotProducedException(dep, provider.name)
prod_paths = self.dep_paths[product.name]
if (prod_paths is not None) and req_exists(paths) and self.f4cache:
_cache_deps(prod_paths, self.f4cache)
return True
def execute(self):
self._build_dep(self.target)
if self.f4cache:
_cache_deps(self.dep_paths[self.target], self.f4cache)
update_dep_statuses(self.dep_paths[self.target], '__target',
self.f4cache)
sfprint(0, f'Target {Style.BRIGHT + self.target + Style.RESET_ALL} '
f'-> {self.dep_paths[self.target]}')
def display_dep_info(stages: 'Iterable[Stage]'):
sfprint(0, 'Platform dependencies/targets:')
longest_out_name_len = 0
for stage in stages:
for out in stage.produces:
l = len(out.name)
if l > longest_out_name_len:
longest_out_name_len = l
desc_indent = longest_out_name_len + 7
nl_indentstr = '\n'
for _ in range(0, desc_indent):
nl_indentstr += ' '
for stage in stages:
for out in stage.produces:
pname = Style.BRIGHT + out.name + Style.RESET_ALL
indent = ''
for _ in range(0, desc_indent - len(pname) + 3):
indent += ' '
specstr = '???'
if out.spec == 'req':
specstr = f'{Fore.BLUE}guaranteed{Fore.RESET}'
elif out.spec == 'maybe':
specstr = f'{Fore.YELLOW}not guaranteed{Fore.RESET}'
elif out.spec == 'demand':
specstr = f'{Fore.RED}on-demand{Fore.RESET}'
pgen = f'{Style.DIM}stage: `{stage.name}`, '\
f'spec: {specstr}{Style.RESET_ALL}'
pdesc = stage.meta[out.name].replace('\n', nl_indentstr)
sfprint(0, f' {Style.BRIGHT + out.name + Style.RESET_ALL}:'
f'{indent}{pdesc}{nl_indentstr}{pgen}')
def display_stage_info(stage: Stage):
if stage is None:
sfprint(0, f'Stage does not exist')
f4pga_fail()
return
sfprint(0, f'Stage `{Style.BRIGHT}{stage.name}{Style.RESET_ALL}`:')
sfprint(0, f' Module: `{Style.BRIGHT}{stage.module.name}{Style.RESET_ALL}`')
sfprint(0, f' Module info:')
mod_info = get_module_info(stage.module)
mod_info = '\n '.join(mod_info.split('\n'))
sfprint(0, f' {mod_info}')
f4pga_done_str = Style.BRIGHT + Fore.GREEN + 'DONE'
def f4pga_fail():
global f4pga_done_str
f4pga_done_str = Style.BRIGHT + Fore.RED + 'FAILED'
def f4pga_done():
sfprint(1, f'f4pga: {f4pga_done_str}'
f'{Style.RESET_ALL + Fore.RESET}')
exit(0)
def setup_resolution_env():
""" Sets up a ResolutionEnv with default built-ins. """
r_env = ResolutionEnv({
'shareDir': share_dir_path,
'binDir': bin_dir_path
})
def _noisy_warnings():
"""
Emit some noisy warnings.
"""
environ['OUR_NOISY_WARNINGS'] = 'noisy_warnings.log'
return 'noisy_warnings.log'
def _generate_values():
"""
Generate initial values, available in configs.
"""
conf = {
'python3': common_sub('which', 'python3').decode().replace('\n', ''),
'noisyWarnings': _noisy_warnings()
}
if (FPGA_FAM == 'xc7'):
conf['prjxray_db'] = common_sub('prjxray-config').decode().replace('\n', '')
return conf
r_env.add_values(_generate_values())
return r_env
def open_project_flow_config(path: str) -> ProjectFlowConfig:
try:
flow_cfg = open_project_flow_cfg(path)
except FileNotFoundError as _:
fatal(-1, 'The provided flow configuration file does not exist')
return flow_cfg
def verify_part_stage_params(flow_cfg: FlowConfig,
part: 'str | None' = None):
if part:
platform_name = get_platform_name_for_part(part)
if not verify_platform_name(platform_name, str(ROOT)):
sfprint(0, f'Platform `{part}`` is unsupported.')
return False
if part not in flow_cfg.part():
sfprint(0, f'Platform `{part}`` is not in project.')
return False
return True
def get_platform_name_for_part(part_name: str):
"""
Gets a name that identifies the platform setup required for a specific chip.
The reason for such distinction is that plenty of chips with different names
differ only in a type of package they use.
"""
with (ROOT / 'part_db.yml').open('r') as rfptr:
for key, val in yaml_load(rfptr, yaml_loader).items():
if part_name.upper() in val:
return key
raise Exception(f"Unknown part name <{part_name}>!")
def make_flow_config(project_flow_cfg: ProjectFlowConfig, part_name: str) -> FlowConfig:
""" Create `FlowConfig` from given project flow configuration and part name """
platform = get_platform_name_for_part(part_name)
if platform is None:
raise F4PGAException(
message='You have to specify a part name or configure a default part.'
)
if part_name not in project_flow_cfg.parts():
raise F4PGAException(
message='Project flow configuration does not support requested part.'
)
r_env = setup_resolution_env()
r_env.add_values({'part_name': part_name.lower()})
scan_modules(str(ROOT))
with (ROOT / 'platforms.yml').open('r') as rfptr:
platforms = yaml_load(rfptr, yaml_loader)
if platform not in platforms:
raise F4PGAException(message=f'Flow definition for platform <{platform}> cannot be found!')
flow_cfg = FlowConfig(
project_flow_cfg,
FlowDefinition(platforms[platform], r_env),
part_name
)
if len(flow_cfg.stages) == 0:
raise F4PGAException(message = 'Platform flow does not define any stage')
return flow_cfg
def cmd_build(args: Namespace):
""" `build` command implementation """
project_flow_cfg: ProjectFlowConfig = None
part_name = args.part
if args.flow:
project_flow_cfg = open_project_flow_config(args.flow)
elif part_name is not None:
project_flow_cfg = ProjectFlowConfig('.temp.flow.json')
project_flow_cfg.flow_cfg = get_cli_flow_config(args, part_name)
if part_name is None and project_flow_cfg is not None:
part_name = project_flow_cfg.get_default_part()
if project_flow_cfg is None:
fatal(-1, 'No configuration was provided. Use `--flow`, and/or '
'`--part` to configure flow.')
flow_cfg = make_flow_config(project_flow_cfg, part_name)
if args.info:
display_dep_info(flow_cfg.stages.values())
f4pga_done()
if args.stageinfo:
display_stage_info(flow_cfg.stages.get(args.stageinfo[0]))
f4pga_done()
target = args.target
if target is None:
target = project_flow_cfg.get_default_target(part_name)
if target is None:
fatal(-1, 'Please specify desired target using `--target` option '
'or configure a default target.')
flow = Flow(
target=target,
cfg=flow_cfg,
f4cache=F4Cache(F4CACHEPATH) if not args.nocache else None
)
dep_print_verbosity = 0 if args.pretend else 2
sfprint(dep_print_verbosity, '\nProject status:')
flow.print_resolved_dependencies(dep_print_verbosity)
sfprint(dep_print_verbosity, '')
if args.pretend:
f4pga_done()
try:
flow.execute()
except AssertionError as e:
raise e
except Exception as e:
sfprint(0, f'{e}')
f4pga_fail()
if flow.f4cache:
flow.f4cache.save()
def cmd_show_dependencies(args: Namespace):
""" `showd` command implementation """
flow_cfg = open_project_flow_config(args.flow)
if not verify_part_stage_params(flow_cfg, args.part):
f4pga_fail()
return
platform_overrides: 'set | None' = None
if args.platform is not None:
platform_overrides = \
set(flow_cfg.get_dependency_platform_overrides(args.part).keys())
display_list = []
raw_deps = flow_cfg.get_dependencies_raw(args.platform)
for dep_name, dep_paths in raw_deps.items():
prstr: str
if (platform_overrides is not None) and (dep_name in platform_overrides):
prstr = f'{Style.DIM}({args.platform}){Style.RESET_ALL} ' \
f'{Style.BRIGHT + dep_name + Style.RESET_ALL}: {dep_paths}'
else:
prstr = f'{Style.BRIGHT + dep_name + Style.RESET_ALL}: {dep_paths}'
display_list.append((dep_name, prstr))
display_list.sort(key = lambda p: p[0])
for _, prstr in display_list:
sfprint(0, prstr)
set_verbosity_level(-1)
def main():
parser = setup_argparser()
args = parser.parse_args()
set_verbosity_level(args.verbose - (1 if args.silent else 0))
if args.command == 'build':
cmd_build(args)
f4pga_done()
if args.command == 'showd':
cmd_show_dependencies(args)
f4pga_done()
sfprint(0, 'Please use a command.\nUse `--help` flag to learn more.')
f4pga_done()
if __name__ == '__main__':
main()

View File

@ -21,7 +21,7 @@ from pathlib import Path
from zlib import adler32 as zlib_adler32
from json import dump as json_dump, load as json_load, JSONDecodeError
from f4pga.common import sfprint
from f4pga.flows.common import sfprint
def _get_hash(path: Path):
if not path.is_dir():
@ -69,7 +69,7 @@ class F4Cache:
if not self.status.get(path):
self.status[path] = {}
self.status[path][consumer] = status
def process_file(self, path: Path):
""" Process file for tracking with f4cache. """

View File

@ -19,8 +19,8 @@
from pathlib import Path
from f4pga.common import vpr_specific_values, vpr as common_vpr, VprArgs
from f4pga.module import Module, ModuleContext
from f4pga.flows.common import vpr_specific_values, vpr as common_vpr, VprArgs
from f4pga.flows.module import Module, ModuleContext
def analysis_merged_post_implementation_file(ctx: ModuleContext):

View File

@ -19,8 +19,8 @@
from pathlib import Path
from f4pga.common import vpr_specific_values, VprArgs, get_verbosity_level, sub as common_sub
from f4pga.module import Module, ModuleContext
from f4pga.flows.common import vpr_specific_values, VprArgs, get_verbosity_level, sub as common_sub
from f4pga.flows.module import Module, ModuleContext
class FasmModule(Module):

View File

@ -59,8 +59,8 @@ Accepted module parameters:
from pathlib import Path
from re import match as re_match, finditer as re_finditer
from f4pga.common import decompose_depname, deep, get_verbosity_level, sub
from f4pga.module import Module, ModuleContext
from f4pga.flows.common import decompose_depname, deep, get_verbosity_level, sub
from f4pga.flows.module import Module, ModuleContext
def _get_param(params, name: str):

View File

@ -40,8 +40,8 @@ Accepted module parameters:
"""
from f4pga.module import Module, ModuleContext
from f4pga.module_runner import get_module
from f4pga.flows.module import Module, ModuleContext
from f4pga.flows.module_runner import get_module
def _switch_keys(d: 'dict[str, ]', renames: 'dict[str, str]') -> 'dict[str, ]':

View File

@ -27,7 +27,7 @@ the dependency algorithm to lazily create the directories if they become necessa
from pathlib import Path
from f4pga.module import Module, ModuleContext
from f4pga.flows.module import Module, ModuleContext
class MkDirsModule(Module):

View File

@ -19,8 +19,8 @@
from pathlib import Path
from f4pga.common import vpr_specific_values, noisy_warnings, vpr as common_vpr, VprArgs
from f4pga.module import Module, ModuleContext
from f4pga.flows.common import vpr_specific_values, noisy_warnings, vpr as common_vpr, VprArgs
from f4pga.flows.module import Module, ModuleContext
DEFAULT_TIMING_RPT = 'pre_pack.report_timing.setup.rpt'

View File

@ -20,8 +20,8 @@
from pathlib import Path
from re import match as re_match
from f4pga.common import vpr_specific_values, vpr as common_vpr, VprArgs, save_vpr_log
from f4pga.module import Module, ModuleContext
from f4pga.flows.common import vpr_specific_values, vpr as common_vpr, VprArgs, save_vpr_log
from f4pga.flows.module import Module, ModuleContext
def default_output_name(place_constraints):

View File

@ -18,8 +18,9 @@
# SPDX-License-Identifier: Apache-2.0
from pathlib import Path
from f4pga.common import sub as common_sub
from f4pga.module import Module, ModuleContext
from f4pga.flows.common import sub as common_sub
from f4pga.flows.module import Module, ModuleContext
class PlaceConstraintsModule(Module):

View File

@ -19,8 +19,8 @@
from pathlib import Path
from f4pga.common import vpr_specific_values, vpr as common_vpr, VprArgs, options_dict_to_list, save_vpr_log
from f4pga.module import Module, ModuleContext
from f4pga.flows.common import vpr_specific_values, vpr as common_vpr, VprArgs, options_dict_to_list, save_vpr_log
from f4pga.flows.module import Module, ModuleContext
def route_place_file(ctx: ModuleContext):

View File

@ -20,8 +20,8 @@
from os import environ
from pathlib import Path
from f4pga.common import decompose_depname, get_verbosity_level, sub as common_sub
from f4pga.module import Module, ModuleContext
from f4pga.flows.common import decompose_depname, get_verbosity_level, sub as common_sub
from f4pga.flows.module import Module, ModuleContext
from f4pga.wrappers.tcl import get_script_path as get_tcl_wrapper_path
@ -83,8 +83,6 @@ class SynthModule(Module):
mapping['json'] = top + '.json'
mapping['synth_json'] = top + '_io.json'
b_path = Path(top).parent.name
for extra in self.extra_products:
name, spec = decompose_depname(extra)
if spec == 'maybe':
@ -93,7 +91,7 @@ class SynthModule(Module):
f'(?) specifier. Product causing this error: `{extra}`.'
)
elif spec == 'req':
mapping[name] = str(Path(b_path) / f'{ctx.values.device}_{name}.{name}')
mapping[name] = str(Path(top).parent / f'{ctx.values.device}_{name}.{name}')
return mapping

View File

@ -22,8 +22,8 @@ from copy import copy
from os import listdir as os_listdir
from json import dump as json_dump, load as json_load
from f4pga.common import ResolutionEnv, deep
from f4pga.stage import Stage
from f4pga.flows.common import ResolutionEnv, deep
from f4pga.flows.stage import Stage
def open_flow_cfg(path: str) -> dict:
@ -135,7 +135,7 @@ class ProjectFlowConfig:
vals = stage_cfg.get('values')
if vals is not None:
stage_vals_ovds.update(vals)
return stage_vals_ovds
def get_dependency_platform_overrides(self, part: str):

View File

@ -24,7 +24,7 @@ Here are the things necessary to write an F4PGA Module.
from types import SimpleNamespace
from abc import abstractmethod
from f4pga.common import (
from f4pga.flows.common import (
decompose_depname,
ResolutionEnv,
fatal

View File

@ -17,10 +17,12 @@
#
# SPDX-License-Identifier: Apache-2.0
from f4pga.module import Module
from f4pga.common import decompose_depname
from colorama import Style
from f4pga.flows.module import Module
from f4pga.flows.common import decompose_depname
def _get_if_qualifier(deplist: 'list[str]', qualifier: str):
for dep_name in deplist:
name, q = decompose_depname(dep_name)

View File

@ -27,8 +27,8 @@ from pathlib import Path
from colorama import Style
from f4pga.module import Module, ModuleContext, get_mod_metadata
from f4pga.common import ResolutionEnv, deep, sfprint
from f4pga.flows.module import Module, ModuleContext, get_mod_metadata
from f4pga.flows.common import ResolutionEnv, deep, sfprint
@contextmanager

View File

@ -0,0 +1,2 @@
colorama
pyyaml

View File

@ -17,9 +17,9 @@
#
# SPDX-License-Identifier: Apache-2.0
from f4pga.common import decompose_depname, resolve_modstr
from f4pga.module import Module
from f4pga.module_runner import get_module, module_io
from f4pga.flows.common import decompose_depname, resolve_modstr
from f4pga.flows.module import Module
from f4pga.flows.module_runner import get_module, module_io
class StageIO:
"""
@ -63,7 +63,7 @@ class Stage:
def __init__(self, name: str, stage_def: 'dict[str, ]'):
if stage_def is None:
stage_def = {}
modstr = stage_def['module']
module_path = resolve_modstr(modstr)

View File

@ -1,2 +1 @@
colorama
pyyaml
-r ./flows/requirements.txt

View File

@ -80,13 +80,14 @@ setuptools_setup(
url="https://github.com/chipsalliance/f4pga",
packages=[
"f4pga",
"f4pga.common_modules",
"f4pga.flows",
"f4pga.flows.common_modules",
"f4pga.wrappers.sh",
"f4pga.wrappers.tcl"
],
package_dir={"f4pga": "."},
package_data={
'f4pga': [
'f4pga.flows': [
'*.yml',
],
'f4pga.wrappers.sh': [

View File

@ -25,22 +25,16 @@ from pathlib import Path
from shutil import which
from subprocess import check_call
from f4pga import FPGA_FAM
from f4pga.context import FPGA_FAM, F4PGA_INSTALL_DIR_PATH
python3 = which('python3')
f4pga_environ = environ.copy()
ROOT = Path(__file__).resolve().parent
isQuickLogic = FPGA_FAM != 'xc7'
SH_SUBDIR = 'quicklogic' if isQuickLogic else FPGA_FAM
F4PGA_INSTALL_DIR = f4pga_environ.get('F4PGA_INSTALL_DIR')
if F4PGA_INSTALL_DIR is None:
raise(Exception("Required environment variable F4PGA_INSTALL_DIR is undefined!"))
F4PGA_INSTALL_DIR_PATH = Path(F4PGA_INSTALL_DIR)
f4pga_environ = environ.copy()
f4pga_environ['F4PGA_SHARE_DIR'] = f4pga_environ.get('F4PGA_SHARE_DIR', str(F4PGA_INSTALL_DIR_PATH / FPGA_FAM / 'share/f4pga'))

View File

@ -20,7 +20,8 @@
# TCL scripts moved from f4pga-arch-defs
from pathlib import Path
from f4pga import FPGA_FAM
from f4pga.context import FPGA_FAM
ROOT = Path(__file__).resolve().parent