diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bd44b99..6068590 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,4 +27,6 @@ jobs: pylint -s n openflow git diff --check --cached - name: Test - run: pytest test + run: | + pytest test + make -C examples/configure diff --git a/Makefile b/Makefile index 846cb1b..b1ea078 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,11 @@ #!/usr/bin/make check: - pycodestyle openflow - pylint -s n openflow + pycodestyle openflow examples + pylint -s n openflow examples git diff --check --cached pytest test + make -C examples/configure clean: py3clean . diff --git a/README.md b/README.md index 5244613..336a85a 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,33 @@ Currently, it is based on `GHDL`, `Yosys`, `ghdl-yosys-plugin`, `nextpnr`, > **NOTE:** it started as part of [PyFPGA](https://github.com/PyFPGA/pyfpga) > and will be used to solves the `openflow` **tool**. + +## Installation + +Openflow requires Python `>=3.6`. For now, it's only available as a git repository +hosted on GitHub. It can be installed with pip: + +``` +pip install 'git+https://github.com/PyFPGA/pyfpga#egg=pyfpga' +``` + +> On GNU/Linux, installing pip packages on the system requires `sudo`. +> Alternatively, use `--local` for installing PyFPGA in your HOME. + +You can get a copy of the repository either through git clone or downloading a +tarball/zipfile: + +``` +git clone https://github.com/PyFPGA/openflow.git +cd openflow +``` + +Then, use pip from the root of the repo: + +``` +pip install -e . +``` + +> With `-e` (`--editable`) your application is installed into site-packages via +> a kind of symlink. That allows pulling changes through git or changing the +> branch, without the need to reinstall the package. diff --git a/examples/configure/Makefile b/examples/configure/Makefile new file mode 100644 index 0000000..8ede04d --- /dev/null +++ b/examples/configure/Makefile @@ -0,0 +1,8 @@ +EXAMPLES=$(wildcard *.py) + +.PHONY: $(EXAMPLES) + +all: $(EXAMPLES) + +$(EXAMPLES): + python3 $@ diff --git a/examples/configure/defaults.py b/examples/configure/defaults.py new file mode 100644 index 0000000..b2d5ce4 --- /dev/null +++ b/examples/configure/defaults.py @@ -0,0 +1,8 @@ +"""Example about default values.""" + +from openflow.configure import ConfigureTools + +cfg = ConfigureTools() + +for tool in cfg.get_tools(): + print(cfg.get_command(tool)) diff --git a/examples/configure/file.py b/examples/configure/file.py new file mode 100644 index 0000000..5eae3ac --- /dev/null +++ b/examples/configure/file.py @@ -0,0 +1,8 @@ +"""Example about to specify a file.""" + +from openflow.configure import ConfigureTools + +cfg = ConfigureTools('file.yml') + +for tool in cfg.get_tools(): + print(cfg.get_command(tool)) diff --git a/examples/configure/file.yml b/examples/configure/file.yml new file mode 100644 index 0000000..0452b32 --- /dev/null +++ b/examples/configure/file.yml @@ -0,0 +1,15 @@ +engine: + name: example + volumes: + - "path1:path2" + - "path3:path4" + work: . + options: global-option +tools: + example1: + name: alt-example1 + container: hdlc/example1 + example2: + name: alt-example2 + container: hdlc/example2 + options: local-option diff --git a/examples/configure/methods.py b/examples/configure/methods.py new file mode 100644 index 0000000..a359144 --- /dev/null +++ b/examples/configure/methods.py @@ -0,0 +1,36 @@ +"""Example about to use the different methods.""" + +from openflow.configure import ConfigureTools + +cfg = ConfigureTools() + +print('* Defaults for GHDL:') +print(cfg.get_command('ghdl')) + +print('* Setting a different engine:') +cfg.set_engine('podman') +print(cfg.get_command('ghdl')) + +print('* Setting different volumes:') +cfg.set_volumes(['v1:v1', 'v2:v2']) +print(cfg.get_command('ghdl')) + +print('* Setting a different work:') +cfg.set_work('/tmp') +print(cfg.get_command('ghdl')) + +print('* Setting a global options:') +cfg.set_global_options('--global option') +print(cfg.get_command('ghdl')) + +print('* Setting a new container:') +cfg.set_container('ghdl', 'alt-ghdl-container') +print(cfg.get_command('ghdl')) + +print('* Setting a new tool name:') +cfg.set_name('ghdl', 'alt-ghdl-name') +print(cfg.get_command('ghdl')) + +print('* Setting a local options:') +cfg.set_local_options('ghdl', '--local option') +print(cfg.get_command('ghdl')) diff --git a/openflow/__init__.py b/openflow/__init__.py index 9ba9573..7111003 100644 --- a/openflow/__init__.py +++ b/openflow/__init__.py @@ -3,3 +3,4 @@ __version__ = '0.1.0' from openflow.openflow import Openflow +from openflow.configure import ConfigureTools diff --git a/openflow/configure.py b/openflow/configure.py new file mode 100644 index 0000000..4ebea8f --- /dev/null +++ b/openflow/configure.py @@ -0,0 +1,105 @@ +# +# Copyright (C) 2020-2021 Rodrigo A. Melo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +"""openflow.configure + +A Class to configure the OCI engine, the containers and the name of the +underlying FOSS tools. +""" + + +import os +from yaml import safe_load, dump + + +class ConfigureTools: + """Configure Tools.""" + + def __init__(self, filename='.openflow.yml'): + """Class constructor.""" + userfile = os.getenv('OPENFLOW_FILE') + homefile = os.path.join(os.path.expanduser('~'), filename) + projfile = os.path.join(os.path.dirname(__file__), 'configure.yml') + if userfile is not None and os.path.exists(userfile): + filepath = userfile + elif os.path.exists(filename): + filepath = filename + elif os.path.exists(homefile): + filepath = homefile + else: + filepath = projfile + self.configs = {} + with open(filepath, 'r') as file: + self.configs = safe_load(file) + + def get_command(self, tool): + """Get the command-line needed to run a tool.""" + engine = self.configs['engine']['name'] + name = self.configs['tools'][tool]['name'] + if engine is not None and os.getenv('OPENFLOW_OFF') is None: + oci = [ + engine, + 'run --rm', + '-v ' + (' -v ').join(self.configs['engine']['volumes']), + '-w ' + self.configs['engine']['work'], + self.configs['engine'].get('options', None), + self.configs['tools'][tool].get('options', None), + self.configs['tools'][tool]['container'], + name + ] + return ' '.join(list(filter(None, oci))) + return name + + def get_tools(self): + """Returns the list of configured tools.""" + return sorted(list(self.configs['tools'].keys())) + + def dump(self): + """Dumps the configuration in YAML format (debug purpouses).""" + return dump(self.configs) + + def set_engine(self, engine): + """Set the OCI engine.""" + self.configs['engine']['name'] = engine + + def unset_engine(self): + """Unset the OCI engine. """ + self.configs['engine']['name'] = None + + def set_volumes(self, volumes): + """Set the volumes of the OCI engine.""" + self.configs['engine']['volumes'] = volumes + + def set_work(self, work): + """Set the working directory inside the container.""" + self.configs['engine']['work'] = work + + def set_global_options(self, options): + """Set options shared by all the containers.""" + self.configs['engine']['options'] = options + + def set_container(self, tool, container): + """Set the container of the specified tool.""" + self.configs['tools'][tool]['container'] = container + + def set_name(self, tool, name): + """Set the name of the specified tool.""" + self.configs['tools'][tool]['name'] = name + + def set_local_options(self, tool, options): + """Set options for a particular container.""" + self.configs['tools'][tool]['options'] = options diff --git a/openflow/configure.yml b/openflow/configure.yml new file mode 100644 index 0000000..6ac1f96 --- /dev/null +++ b/openflow/configure.yml @@ -0,0 +1,35 @@ +engine: + name: docker + volumes: + - "$HOME:$HOME" + work: $PWD +tools: + ecppack: + name: ecppack + container: hdlc/prjtrellis + ghdl: + name: ghdl + container: hdlc/ghdl:yosys + icepack: + name: icepack + container: hdlc/icestorm + iceprog: + name: iceprog + container: hdlc/icestorm + icetime: + name: icetime + container: hdlc/icestorm + nextpnr-ecp5: + name: nextpnr-ecp5 + container: hdlc/nextpnr:ecp5 + nextpnr-ice40: + name: nextpnr-ice40 + container: hdlc/nextpnr:ice40 + openocd: + name: openocd + container: hdlc/prog + options: "--device /dev/bus/usb" + yosys: + name: yosys + container: hdlc/ghdl:yosys + options: "--device /dev/bus/usb" diff --git a/openflow/helpers/configure.py b/openflow/helpers/configure.py new file mode 100644 index 0000000..c3a941c --- /dev/null +++ b/openflow/helpers/configure.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 Rodrigo A. Melo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +""" +A CLI to get the full OCI command for the specified tool. +""" + +import argparse + +from openflow import __version__ as version +from openflow.configure import ConfigureTools + + +def main(): + """Solves the main functionality of this helper.""" + + cfg = ConfigureTools() + + # Parsing the command-line. + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + parser.add_argument( + '-v', '--version', + action='version', + version='v{}'.format(version) + ) + + parser.add_argument( + 'tool', + metavar='TOOL', + choices=cfg.get_tools(), + help=', '.join(cfg.get_tools()) + ) + + args = parser.parse_args() + + # Solving the functionality + + print(cfg.get_command(args.tool)) + + +if __name__ == "__main__": + main() diff --git a/openflow/openflow.py b/openflow/openflow.py index 5ebd616..a9f5316 100644 --- a/openflow/openflow.py +++ b/openflow/openflow.py @@ -28,6 +28,51 @@ def __init__(self, name='openflow', part='hx8k-ct256'): """Class constructor.""" self.name = name self.part = get_part(part) + self.set_oci() + self.set_tools() + + def set_oci(self): + """Set the OCI engine.""" + self.oci = { + 'engine': 'docker', + 'volumes': ['$HOME:$HOME'], + 'work': '$PWD', + 'containers': { + 'ghdl': 'hdlc/ghdl:yosys', + 'yosys': 'hdlc/ghdl:yosys', + 'nextpnr-ice40': 'hdlc/nextpnr:ice40', + 'icetime': 'hdlc/icestorm', + 'icepack': 'hdlc/icestorm', + 'iceprog': 'hdlc/icestorm', + 'nextpnr-ecp5': 'hdlc/nextpnr:ecp5', + 'ecppack': 'hdlc/prjtrellis', + 'openocd': 'hdlc/prog' + }, + 'tools': { + 'ghdl': 'ghdl', + 'yosys': 'yosys', + 'nextpnr-ice40': 'nextpnr-ice40', + 'icetime': 'icetime', + 'icepack': 'icepack', + 'iceprog': 'iceprog', + 'nextpnr-ecp5': 'nextpnr-ecp5', + 'ecppack': 'ecppack', + 'openocd': 'openocd' + } + } + + def set_tools(self): + """Set the underlying tools.""" + # Check if oci['engine'] is available + engine = self.oci['engine'] + volumes = '-v ' + ('-v ').join(self.oci['volumes']) + work = '-w ' + self.oci['work'] + command = '{} run --rm {} {}'.format(engine, volumes, work) + self.tools = {} + for tool in self.oci['tools']: + self.tools[tool] = '{} {} {}'.format( + command, self.oci['containers'][tool], self.oci['tools'][tool] + ) # pylint: disable=too-many-arguments def synthesis( diff --git a/setup.py b/setup.py index 397d563..de8313e 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,15 @@ +import re from setuptools import setup, find_packages -import openflow +with open('openflow/__init__.py', 'r') as f: + version = re.search(r"__version__ = '([\d\.]*)'", f.read()).group(1) -with open('README.md', 'r') as fh: - long_description = fh.read() +with open('README.md', 'r') as f: + long_description = f.read() setup( name='fpgaopenflow', - version=openflow.__version__, + version=version, description='a Python library, and CLI utilities, which solves HDL-to-bitstream based on FOSS', long_description=long_description, long_description_content_type='text/markdown', @@ -16,11 +18,13 @@ license='GPLv3', url='https://github.com/PyFPGA/openflow', packages=find_packages(), + package_data={'': ['configure.yml']}, entry_points={ 'console_scripts': [ 'openflow_syn = openflow.helpers.synthesis:main', 'openflow_imp = openflow.helpers.implementation:main', - 'openflow_bit = openflow.helpers.bitstream:main' + 'openflow_bit = openflow.helpers.bitstream:main', + 'openflow_cfg = openflow.helpers.configure:main' ], }, classifiers=[ @@ -36,5 +40,7 @@ 'Topic :: Utilities', 'Topic :: Software Development :: Build Tools', "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)" - ] + ], + install_requires=['pyyaml'], + python_requires=">=3.5, <4" )