back.verilog: fall back to nmigen_yosys package.

The nmigen-yosys PyPI package provides a custom, minimal build of
Yosys that uses (at the moment) wasmtime-py to deliver a single
WASM binary that can run on many platforms, and eliminates the need
to build Yosys from source.

Not only does this lower barrier to entry for new nMigen developers,
but also decouples nMigen from Yosys' yearly release cycle, which
lets us use new features and drop workarounds for Yosys bugs earlier.

The source for the nmigen-yosys package is provided at:
  https://github.com/nmigen/nmigen-yosys
The package is built from upstream source and released automatically
with no manual steps.

Fixes #371.
This commit is contained in:
whitequark 2020-05-22 16:50:45 +00:00
parent eaf33fb6b5
commit b9799b4c4a
3 changed files with 190 additions and 41 deletions

176
nmigen/_yosys.py Normal file
View file

@ -0,0 +1,176 @@
import os
import sys
import re
import subprocess
try:
from importlib import metadata as importlib_metadata # py3.8+ stdlib
except ImportError:
try:
import importlib_metadata # py3.7- shim
except ImportError:
importlib_metadata = None # not installed
from ._toolchain import has_tool, require_tool
__all__ = ["YosysError", "YosysBinary", "find_yosys"]
class YosysError(Exception):
pass
class YosysBinary:
@classmethod
def available(cls):
"""Check for Yosys availability.
Returns
-------
available : bool
``True`` if Yosys is installed, ``False`` otherwise. Installed binary may still not
be runnable, or might be too old to be useful.
"""
raise NotImplementedError
@classmethod
def version(cls):
"""Get Yosys version.
Returns
-------
major : int
Major version.
minor : int
Minor version.
distance : int
Distance to last tag per ``git describe``. May not be exact for system Yosys.
"""
raise NotImplementedError
@classmethod
def run(cls, args, stdin=""):
"""Run Yosys process.
Parameters
----------
args : list of str
Arguments, not including the program name.
stdin : str
Standard input.
Returns
-------
stdout : str
Standard output.
Exceptions
----------
YosysError
Raised if Yosys returns a non-zero code. The exception message is the standard error
output.
"""
raise NotImplementedError
class _BuiltinYosys(YosysBinary):
YOSYS_PACKAGE = "nmigen_yosys"
@classmethod
def available(cls):
if importlib_metadata is None:
return False
try:
importlib_metadata.version(cls.YOSYS_PACKAGE)
return True
except importlib_metadata.PackageNotFoundError:
return False
@classmethod
def version(cls):
version = importlib_metadata.version(cls.YOSYS_PACKAGE)
match = re.match(r"^(\d+)\.(\d+)(?:\.post(\d+))?", version)
return (int(match[1]), int(match[2]), int(match[3] or 0))
@classmethod
def run(cls, args, stdin=""):
popen = subprocess.Popen([sys.executable, "-m", cls.YOSYS_PACKAGE, *args],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
encoding="utf-8")
stdout, stderr = popen.communicate(stdin)
if popen.returncode:
raise YosysError(stderr.strip())
else:
return stdout
class _SystemYosys(YosysBinary):
YOSYS_BINARY = "yosys"
@classmethod
def available(cls):
return has_tool(cls.YOSYS_BINARY)
@classmethod
def version(cls):
version = cls.run(["-V"])
match = re.match(r"^Yosys (\d+)\.(\d+)(?:\+(\d+))?", version)
return (int(match[1]), int(match[2]), int(match[3] or 0))
@classmethod
def run(cls, args, stdin=""):
popen = subprocess.Popen([require_tool(cls.YOSYS_BINARY), *args],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
encoding="utf-8")
stdout, stderr = popen.communicate(stdin)
# If Yosys is built with an evaluation version of Verific, then Verific license
# information is printed first. It consists of empty lines and lines starting with `--`,
# which are not normally a part of Yosys output, and can be fairly safely removed.
#
# This is not ideal, but Verific license conditions rule out any other solution.
stdout = re.sub(r"\A(-- .+\n|\n)*", "", stdout)
if popen.returncode:
raise YosysError(stderr.strip())
else:
return stdout
def find_yosys(requirement):
"""Find an available Yosys executable of required version.
Parameters
----------
requirement : function
Version check. Should return ``True`` if the version is acceptable, ``False`` otherwise.
Returns
-------
yosys_binary : subclass of YosysBinary
Proxy for running the requested version of Yosys.
Exceptions
----------
YosysError
Raised if required Yosys version is not found.
"""
proxies = []
clauses = os.environ.get("NMIGEN_USE_YOSYS", "system,builtin").split(",")
for clause in clauses:
if clause == "builtin":
proxies.append(_BuiltinYosys)
elif clause == "system":
proxies.append(_SystemYosys)
else:
raise YosysError("The NMIGEN_USE_YOSYS environment variable contains "
"an unrecognized clause {!r}"
.format(clause))
for proxy in proxies:
if proxy.available() and requirement(proxy.version()):
return proxy
else:
if "NMIGEN_USE_YOSYS" in os.environ:
raise YosysError("Could not find an acceptable Yosys binary. Searched: {}"
.format(", ".join(clauses)))
else:
raise YosysError("Could not find an acceptable Yosys binary. The `nmigen_yosys` PyPI "
"package, if available for this platform, can be used as fallback")

View file

@ -3,31 +3,17 @@ import re
import subprocess import subprocess
import itertools import itertools
from .._toolchain import * from .._yosys import *
from . import rtlil from . import rtlil
__all__ = ["YosysError", "convert", "convert_fragment"] __all__ = ["YosysError", "convert", "convert_fragment"]
class YosysError(Exception):
pass
def _yosys_version():
yosys_path = require_tool("yosys")
version = subprocess.check_output([yosys_path, "-V"], encoding="utf-8")
# If Yosys is built with Verific, then Verific license information is printed first.
# See below for details.
m = re.search(r"^Yosys ([\d.]+)(?:\+(\d+))?", version, flags=re.M)
tag, offset = m[1], m[2] or 0
return tuple(map(int, tag.split("."))), offset
def _convert_rtlil_text(rtlil_text, *, strip_internal_attrs=False, write_verilog_opts=()): def _convert_rtlil_text(rtlil_text, *, strip_internal_attrs=False, write_verilog_opts=()):
version, offset = _yosys_version() # this version requirement needs to be synchronized with the one in setup.py!
if version < (0, 9): yosys = find_yosys(lambda ver: ver >= (0, 9))
raise YosysError("Yosys {}.{} is not supported".format(*version)) yosys_version = yosys.version()
attr_map = [] attr_map = []
if strip_internal_attrs: if strip_internal_attrs:
@ -37,7 +23,7 @@ def _convert_rtlil_text(rtlil_text, *, strip_internal_attrs=False, write_verilog
attr_map.append("-remove nmigen.hierarchy") attr_map.append("-remove nmigen.hierarchy")
attr_map.append("-remove nmigen.decoding") attr_map.append("-remove nmigen.decoding")
script = """ return yosys.run(["-q", "-"], """
# Convert nMigen's RTLIL to readable Verilog. # Convert nMigen's RTLIL to readable Verilog.
read_ilang <<rtlil read_ilang <<rtlil
{} {}
@ -53,28 +39,11 @@ attrmap {attr_map}
attrmap -modattr {attr_map} attrmap -modattr {attr_map}
write_verilog -norename {write_verilog_opts} write_verilog -norename {write_verilog_opts}
""".format(rtlil_text, """.format(rtlil_text,
prune="# " if version == (0, 9) and offset == 0 else "", # Yosys 0.9 release has buggy proc_prune.
prune="# " if yosys_version < (0, 9, 231) else "",
attr_map=" ".join(attr_map), attr_map=" ".join(attr_map),
write_verilog_opts=" ".join(write_verilog_opts), write_verilog_opts=" ".join(write_verilog_opts),
) ))
popen = subprocess.Popen([require_tool("yosys"), "-q", "-"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8")
verilog_text, error = popen.communicate(script)
if popen.returncode:
raise YosysError(error.strip())
else:
# If Yosys is built with an evaluation version of Verific, then Verific license information
# is printed first. It consists of empty lines and lines starting with `--`, which are not
# valid at the start of a Verilog file, and thus may be reliably removed.
verilog_text = "\n".join(itertools.dropwhile(
lambda x: x == "" or x.startswith("--"),
verilog_text.splitlines()
))
return verilog_text
def convert_fragment(*args, strip_internal_attrs=False, **kwargs): def convert_fragment(*args, strip_internal_attrs=False, **kwargs):

View file

@ -23,12 +23,16 @@ setup(
#long_description="""TODO""", #long_description="""TODO""",
license="BSD", license="BSD",
python_requires="~=3.6", python_requires="~=3.6",
setup_requires=["setuptools_scm"], setup_requires=["setuptools", "setuptools_scm"],
install_requires=[ install_requires=[
"setuptools", "importlib_metadata; python_version<'3.8'", # for nmigen._yosys
"pyvcd~=0.2.0", # for nmigen.pysim "pyvcd~=0.2.0", # for nmigen.pysim
"Jinja2~=2.11", # for nmigen.build "Jinja2~=2.11", # for nmigen.build
], ],
extras_require = {
# this version requirement needs to be synchronized with the one in nmigen.back.verilog!
"builtin-yosys": ["nmigen-yosys>=0.9.*"],
},
packages=find_packages(), packages=find_packages(),
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [