ibex/util/check_tool_requirements.py
Philipp Wagner 8b42024cd5 Use vendored-in primitives from OpenTitan
Instead of using copies of primitives from OpenTitan, vendor the files
in directly from OpenTitan, and use them.

Benefits:

- Less potential for diverging code between OpenTitan and Ibex, causing
  problems when importing Ibex into OT.

- Use of the abstract primitives instead of the generic ones. The
  abstract primitives are replaced during synthesis time with
  target-dependent implementations. For simulation, nothing changes. For
  synthesis for a given target technology (e.g. a specific ASIC or FPGA
  technology), the primitives system can be instructed to choose
  optimized versions (if available).

  This is most relevant for the icache, which hard-coded the generic
  SRAM primitive before. This primitive is always implemented as
  registers. By using the abstract primitive (prim_ram_1p) instead, the
  RAMs can be replaced with memory-compiler-generated ones if necessary.

There are no real draw-backs, but a couple points to be aware of:

- Our ram_1p and ram_2p implementations are kept as wrapper around the
  primitives, since their interface deviates slightly from the one in
  prim_ram*. This also includes a rather unfortunate naming confusion
  around rvalid, which means "read data valid" in the OpenTitan advanced
  RAM primitives (prim_ram_1p_adv for example), but means "ack" in
  PULP-derived IP and in our bus implementation.

- The core_ibex UVM DV doesn't use FuseSoC to generate its file list,
  but uses a hard-coded list in `ibex_files.f` instead. Since the
  dynamic primitives system requires the use of FuseSoC we need to
  provide a stop-gap until this file is removed. Issue #893 tracks
  progress on that.

- Dynamic primitives depend no a not-yet-merged feature of FuseSoC
  (https://github.com/olofk/fusesoc/pull/391). We depend on the same
  functionality in OpenTitan and have instructed users to use a patched
  branch of FuseSoC for a long time through `python-requirements.txt`,
  so no action is needed for users which are either successfully
  interacting with the OpenTitan source code, or have followed our
  instructions. All other users will see a reasonably descriptive error
  message during a FuseSoC run.

- This commit is massive, but there are no good ways to split it into
  bisectable, yet small, chunks. I'm sorry. Reviewers can safely ignore
  all code in `vendor/lowrisc_ip`, it's an import from OpenTitan.

- The check_tool_requirements tooling isn't easily vendor-able from
  OpenTitan at the moment. I've filed
  https://github.com/lowRISC/opentitan/issues/2309 to get that sorted.

- The LFSR primitive doesn't have a own core file, forcing us to include
  the catch-all `lowrisc:prim:all` core. I've filed
  https://github.com/lowRISC/opentitan/issues/2310 to get that sorted.
2020-05-27 10:23:15 +01:00

143 lines
4.7 KiB
Python
Executable file

#!/usr/bin/python3
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
from distutils.version import StrictVersion
import logging as log
import os
import re
import subprocess
import sys
# Display INFO log messages and up.
log.basicConfig(level=log.INFO, format="%(levelname)s: %(message)s")
def get_tool_requirements_path():
'''Return the path to tool_requirements.py, at the top of the repo'''
# top_src_dir is the top of the repository
top_src_dir = os.path.normpath(os.path.join(os.path.dirname(__file__),
'..'))
return os.path.join(top_src_dir, 'tool_requirements.py')
def read_tool_requirements(path=None):
'''Read tool requirements from a Python file'''
if path is None:
path = get_tool_requirements_path()
with open(path, 'r') as pyfile:
globs = {}
exec(pyfile.read(), globs)
# We expect the exec call to have populated globs with a
# __TOOL_REQUIREMENTS__ dictionary.
reqs = globs.get('__TOOL_REQUIREMENTS__')
if reqs is None:
log.error('The Python file at {} did not define '
'__TOOL_REQUIREMENTS__.'
.format(path))
return None
# reqs should be a dictionary (mapping tool name to minimum version)
if not isinstance(reqs, dict):
log.error('The Python file at {} defined '
'__TOOL_REQUIREMENTS__, but it is not a dict.'
.format(path))
return None
return reqs
def get_verilator_version():
try:
# Note: "verilator" needs to be called through a shell and with all
# arguments in a string, as it doesn't have a shebang, but instead
# relies on perl magic to parse command line arguments.
version_str = subprocess.run('verilator --version', shell=True,
check=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True)
return version_str.stdout.split(' ')[1].strip()
except subprocess.CalledProcessError as e:
log.error("Unable to call Verilator to check version: " + str(e))
log.error(e.stdout)
return None
def pip3_get_version(tool):
'''Run pip3 to find the version of an installed module'''
cmd = ['pip3', 'show', tool]
try:
proc = subprocess.run(cmd,
check=True,
stderr=subprocess.STDOUT,
stdout=subprocess.PIPE,
universal_newlines=True)
except subprocess.CalledProcessError as err:
log.error('pip3 command failed: {}'.format(err))
log.error("Failed to get version of {} with pip3: is it installed?"
.format(tool))
log.error(err.stdout)
return None
version_re = 'Version: (.*)'
for line in proc.stdout.splitlines():
match = re.match(version_re, line)
if match:
return match.group(1)
# If we get here, we never saw a version line.
log.error('No output line from running {} started with "Version: ".'
.format(cmd))
return None
def check_version(requirements, tool_name, getter):
required_version = requirements.get(tool_name)
if required_version is None:
log.error('Requirements file does not specify version for {}.'
.format(tool_name))
return False
actual_version = getter()
if actual_version is None:
return False
if StrictVersion(actual_version) < StrictVersion(required_version):
log.error("%s is too old: found version %s, need at least %s",
tool_name, actual_version, required_version)
return False
else:
log.info("Found sufficiently recent version of %s (found %s, need %s)",
tool_name, actual_version, required_version)
return True
def main():
# Get tool requirements
tool_requirements = read_tool_requirements()
if tool_requirements is None:
return 1
all_good = True
all_good &= check_version(tool_requirements,
'verilator',
get_verilator_version)
all_good &= check_version(tool_requirements,
'edalize',
lambda: pip3_get_version('edalize'))
if not all_good:
log.error("Tool requirements not fulfilled. "
"Please update the tools and retry.")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())