Tools
As many tools and versions may be declared, the syntax needs to be concise. There are three requirements:
- 
The binaries, libraries, and supporting files that form the tool need to be bound into the container instance;
 - 
Some environment variables may need to be setup to modify the execution behaviour (e.g.
VERILATOR_ROOT); - 
Path-type environment variables need to be extended to include the tool's binary and library directories (e.g.
PATHfor binaries andLD_LIBRARY_PATHfor shared object libraries). 
Blockwork tool declarations are handled in Python, and the following is an example of the syntax:
from pathlib import Path
from blockwork.tools import Tool, Version
@Tool.register()
class Verilator(Tool):
    versions = [
        Version(location = Tool.HOST_ROOT / "verilator-4.106"
                version  = "4.106"
                env      = { "VERILATOR_ROOT": Tool.CNTR_ROOT }
                paths    = { "PATH": [Tool.CNTR_ROOT / "bin"] }),
    ]
Working through this example:
- 
@Tool.register()- associates the tool description with Blockwork's internal registry, allowing it to be used in a flow; - 
class Verilator(Tool):- extends from theToolbase class and defines the name associated with this definition (e.g.Verilator); - 
versions- defines different named versions of a tool; - 
Each version is defined by an instance of
Versionwhere: - 
location- identifies the path on the host where the tool is installed, this path can use theTool.HOST_ROOTvariable that will be resolved to a complete path using the value specified in the configuration; - 
version- sets the version number for the tool, this is to make it distinct from other declarations; - 
env- dictionary of variables to append into the container's shell environment; - 
paths- dictionary of lists, where each list entry is a section to append to a$PATH-type variable within the container's shell environment. 
Note
The Tool.CNTR_ROOT variable points to the equivalent of the location when
mapped into the container (i.e. the root directory of the bound tool)
Tools are mapped into the container using a standard path structure:
/tools/<TOOL_NAME>/<VERSION>
The <TOOL_NAME> will be replaced by a lowercase version of the class name, for
the example given above this would mean <TOOL_NAME> becomes verilator. The
<VERSION> always matches the version field (i.e. 4.106 in this case). For the
Verilator example, this would give a path of:
/tools/verilator/4.106
Vendor Grouping
If a suite of tools from a single supplier, the syntax also allows for the vendor
keyword to be provided which adds an extra section into the path. For example:
@Tool.register()
class Make(Tool):
    vendor   = "GNU"
    versions = [
        Version(location = Tool.HOST_ROOT / "make-4.4",
                version  = "4.4",
                paths    = { "PATH": [Tool.CNTR_ROOT / "bin"] }),
    ]
Will be mapped using the form /tools/<VENDOR>/<TOOL_NAME>/<VERSION> to:
/tools/gnu/make/4.4
Note
Vendor and tool name will always be converted to lowercase, Blockwork will check before binding that no two mapped tools collide
Multiple Versions
When multiple tool versions are defined, there must be one marked as default which will be bound when a version is not explicitly given:
@Tool.register()
class Make(Tool):
    vendor   = "GNU"
    versions = [
        Version(location = Tool.HOST_ROOT / "make-4.4",
                version  = "4.4",
                paths    = { "PATH": [Tool.CNTR_ROOT / "bin"] },
                default  = True),
        Version(location = Tool.HOST_ROOT / "make-4.3",
                version  = "4.3",
                paths    = { "PATH": [Tool.CNTR_ROOT / "bin"] }),
    ]
Warning
If no version is marked as default then a ToolError will be raised. Similarly,
if multiple versions are marked as default then a ToolError will be raised.
Forming Requirements
Tools may rely on other tools to provide binaries or libraries to support their
execution, these relationships are described through Require objects:
from blockwork.tools import Require, Tool, Version
@Tool.register()
class Python(Tool):
    """ Base Python installation """
    versions = [
        Version(location = Tool.HOST_ROOT / "python-3.11.4",
                version  = "3.11.4",
                paths    = { "PATH"           : [Tool.CNTR_ROOT / "bin"],
                             "LD_LIBRARY_PATH": [Tool.CNTR_ROOT / "lib"] })
    ]
@Tool.register()
class PythonSite(Tool):
    """ Versioned package installation """
    versions = [
        Version(location = Tool.HOST_ROOT / "python-site-3.11.4",
                version  = "3.11.4",
                env      = { "PYTHONUSERBASE": Tool.CNTR_ROOT },
                paths    = { "PATH"      : [Tool.CNTR_ROOT / "bin"],
                             "PYTHONPATH": [Tool.CNTR_ROOT / "lib" / "python3.11" / "site-packages"] },
                requires = [Require(Python, "3.11.4")]),
    ]
The Require class takes two arguments:
tool- which must carry aTooldefinition;version- which can either be omitted (implicitly selecting the default version) or can be a string identifying a version number.
Actions and Invocations
Many tools will offer a command line interface that can perform certain discrete tasks, for example a wave viewer like GTKWave will be able to display the contents of a VCD. Such tasks can be wrapped up as an 'action' within a tool declaration, which can then be invoked directly from the command line.
Actions return Invocation objects that encapsulates the command to run, any
arguments to provide, and files or folders to be bound in to the container.
from pathlib import Path
from typing import List
from blockwork.tools import Invocation, Tool, Version
from blockwork.context import Context
@Tool.register()
class GTKWave(Tool):
    versions = [
        Version(location = tool_root / "gtkwave-3.3.113",
                version  = "3.3.113",
                paths    = { "PATH": [Tool.CNTR_ROOT / "src"] }),
    ]
    @Tool.action(default=True)
    def view(self,
             ctx      : Context
             wavefile : str,
             *args    : List[str]) -> Invocation:
        path = Path(wavefile).absolute()
        return Invocation(
            tool    = self,
            execute = Tool.CNTR_ROOT / "src" / "gtkwave",
            args    = [path, *args],
            display = True,
            binds   = [path.parent]
        )
Warning
The name provided as the first argument to @Tool.action() must match the
name of the class that declares the tool.
This action can then be invoked from the shell using the bw tool command:
Or, as view is marked as a default action, this can be shortened to just:
Note
As this action will invoke an X11 GUI, the display = True argument must be
provided in the Invocation instance.
Paths and Binds
The example of the GTKWave view action above relies on reading files from the
host filesystem, this means that they need to be bound into the container prior
to invoking the tool. When an action is invoked it may manually specify binds,
but the arguments list can also contain paths which will be automatically bound
into the container.
Each bound path must be relative to the project root directory on the host, for
example if a project is located under /home/fred/example then all paths bound
in must be under this directory - that is to say /home/fred/example/waves.vcd
is okay, but /home/fred/outside.vcd is not.
Installers
Blockwork provides a special @Tool.installer() decorator for registering a
specific action for installing a tool's binaries/libraries in some way. How a
tool is installed (i.e. by downloading or compiling) is up to the action to
determine.
The example below demonstrates how an installer action can be setup to download the source code for a specific version of Python and compile it.
from pathlib import Path
from blockwork.tools import Invocation, Require, Tool, Version
from blockwork.context import Context
@Tool.register()
class Python(Tool):
    """ Base Python installation """
    versions = [
        Version(location = Tool.HOST_ROOT / "python-3.11.4",
                version  = "3.11.4",
                paths    = { "PATH"           : [Tool.CNTR_ROOT / "bin"],
                             "LD_LIBRARY_PATH": [Tool.CNTR_ROOT / "lib"] })
    ]
    @Tool.installer()
    def install(self, context : Context, *args : List[str]) -> Invocation:
        vernum = self.vernum
        tool_dir = Path("/tools") / self.location.relative_to(TOOL_ROOT)
        script = [
            f"wget --quiet https://www.python.org/ftp/python/{vernum}/Python-{vernum}.tgz",
            f"tar -xf Python-{vernum}.tgz",
            f"cd Python-{vernum}",
            f"./configure --enable-optimizations --with-ensurepip=install "
            f"--enable-shared --prefix={tool_dir.as_posix()}",
            "make -j4",
            "make install",
            "cd ..",
            f"rm -rf Python-{vernum} ./*.tgz*"
        ]
        return Invocation(
            tool    = self,
            execute = "bash",
            args    = ["-c", " && ".join(script)],
            workdir = tool_dir
        )
There is a built-in bootstrapping action that locates and executes all of the tool installation methods: