296 lines
11 KiB
Python
296 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
from lxml import etree
|
|
import chardet
|
|
import sys
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Tuple, List, Set
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import traceback
|
|
import re
|
|
|
|
def usage():
|
|
print("Usage: python3 run-linter.py <path-to-vivado-xpr>", file=sys.stderr)
|
|
|
|
def is_header(f: Path):
|
|
with f.open('rb') as fd:
|
|
return -1 == fd.read().find(b'endmodule')
|
|
|
|
def transcoding(src: Path, target: Path):
|
|
with src.open("rb") as fd:
|
|
rawdata = fd.read()
|
|
res = chardet.detect(rawdata)
|
|
detected = res['encoding'] if res['confidence'] > 0.5 else None
|
|
encodings = ["utf-8", "gb18030"]
|
|
if detected and detected.lower() not in ("utf-8", "gb18030"):
|
|
encodings.append(detected)
|
|
|
|
for encoding in encodings:
|
|
try:
|
|
s = rawdata.decode(encoding)
|
|
break
|
|
except UnicodeDecodeError:
|
|
continue
|
|
else:
|
|
print("Transcoding", src, "with replacement characters")
|
|
s = rawdata.decode("utf-8", "replace")
|
|
with target.open("wb") as wfd:
|
|
wfd.write(s.encode('utf-8'))
|
|
|
|
def parse_project(xpr: Path) -> Tuple[str, Set[str], Set[str], Set[str]]:
|
|
prjname = os.path.splitext(xpr.name)[0]
|
|
prjdir = xpr.parent.resolve()
|
|
repo_root = prjdir.parent.parent
|
|
srcdir = prjdir / (prjname + ".srcs")
|
|
topname = ''
|
|
target = prjdir / '.lint'
|
|
|
|
tree = etree.parse(str(xpr))
|
|
srclist = set()
|
|
headerlist = set()
|
|
inclist = set()
|
|
skipped_ip_count = 0
|
|
for fileset in tree.xpath("/Project/FileSets/FileSet"):
|
|
if fileset.attrib['Type'] != 'DesignSrcs' and \
|
|
fileset.attrib['Type'] != 'BlockSrcs':
|
|
continue
|
|
for child in fileset:
|
|
if child.tag == 'File':
|
|
tmp = child.attrib['Path']
|
|
if tmp.endswith((".xci", ".xcix")):
|
|
skipped_ip_count += 1
|
|
continue
|
|
tmp = tmp.replace('$PSRCDIR', str(srcdir))
|
|
tmp = tmp.replace('$PPRDIR', str(prjdir))
|
|
vlog = Path(tmp).resolve()
|
|
if not vlog.is_file():
|
|
print("Source file", vlog, "does not exist")
|
|
continue
|
|
# An expanded XCI may add DCP/VHDL/netlist artifacts to the
|
|
# project. They are not Verilog lint inputs; use the Verilog
|
|
# stub and regular RTL sources only.
|
|
if vlog.suffix.lower() not in (".v", ".sv", ".vh", ".svh", ".h"):
|
|
continue
|
|
if vlog.name.lower().endswith(("_sim_netlist.v", "_sim_netlist.sv")):
|
|
continue
|
|
vlog_target = target / vlog.relative_to(repo_root)
|
|
vlog_target.parent.mkdir(exist_ok=True, parents=True)
|
|
# shutil.copy(vlog, vlog_target)
|
|
transcoding(vlog, vlog_target)
|
|
inclist.add(str(vlog_target.parent))
|
|
if is_header(vlog_target):
|
|
headerlist.add(str(vlog_target))
|
|
else:
|
|
srclist.add(str(vlog_target))
|
|
elif child.tag == 'Config' and fileset.attrib['Type'] == 'DesignSrcs':
|
|
topname = child.xpath("./Option[@Name='TopModule']")[0].attrib['Val']
|
|
|
|
return (topname, headerlist, srclist, inclist)
|
|
|
|
def verilator_supports_warning(verilator: str, env, warning: str) -> bool:
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
probe = Path(tmpdir) / "verilator_warning_probe.v"
|
|
probe.write_text("module verilator_warning_probe; endmodule\n", encoding="utf-8")
|
|
res = subprocess.run(
|
|
[verilator, "--lint-only", "-Wno-" + warning, str(probe)],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
universal_newlines=True,
|
|
env=env,
|
|
)
|
|
return "Unknown warning specified" not in res.stdout
|
|
|
|
def strip_verilog_comments(text: str) -> str:
|
|
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
|
|
return re.sub(r"//.*", "", text)
|
|
|
|
def find_module_body(text: str, module_name: str) -> str:
|
|
pattern = re.compile(r"\bmodule\s+" + re.escape(module_name) + r"\b.*?\bendmodule\b", re.S)
|
|
m = pattern.search(text)
|
|
return m.group(0) if m else ""
|
|
|
|
def find_top_source(topname: str, srclist: Set[str]) -> Tuple[Path, str]:
|
|
module_re = re.compile(r"\bmodule\s+" + re.escape(topname) + r"\b")
|
|
for src in sorted(srclist):
|
|
path = Path(src)
|
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
if module_re.search(strip_verilog_comments(text)):
|
|
return path, text
|
|
return Path(), ""
|
|
|
|
def module_is_instantiated(module_name: str, text: str) -> bool:
|
|
code = strip_verilog_comments(text)
|
|
pattern = re.compile(
|
|
r"(?<!\bmodule\s)\b"
|
|
+ re.escape(module_name)
|
|
+ r"\s*(?:#\s*\(.*?\)\s*)?[A-Za-z_][A-Za-z0-9_$]*\s*\(",
|
|
re.S,
|
|
)
|
|
return bool(pattern.search(code))
|
|
|
|
def extract_module_definitions(sources: Set[str]) -> Tuple[dict, dict]:
|
|
module_sources = {}
|
|
module_bodies = {}
|
|
for src in sorted(sources):
|
|
path = Path(src)
|
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
code = strip_verilog_comments(text)
|
|
for m in re.finditer(r"\bmodule\s+([A-Za-z_][A-Za-z0-9_$]*)\b.*?\bendmodule\b", code, re.S):
|
|
name = m.group(1)
|
|
module_sources.setdefault(name, path)
|
|
module_bodies.setdefault(name, m.group(0))
|
|
return module_sources, module_bodies
|
|
|
|
def find_instantiated_modules(text: str, module_names: Set[str]) -> Set[str]:
|
|
return {
|
|
module
|
|
for module in module_names
|
|
if module_is_instantiated(module, text)
|
|
}
|
|
|
|
def find_reachable_modules(topname: str, module_bodies: dict) -> Set[str]:
|
|
reachable = set()
|
|
pending = [topname]
|
|
module_names = set(module_bodies)
|
|
while pending:
|
|
module = pending.pop()
|
|
if module in reachable:
|
|
continue
|
|
reachable.add(module)
|
|
body = module_bodies.get(module, "")
|
|
for child in find_instantiated_modules(body, module_names - reachable):
|
|
pending.append(child)
|
|
return reachable
|
|
|
|
def run_design_sanity_checks(prjdir: Path, topname: str, srclist: Set[str]):
|
|
issues = []
|
|
top_source, top_text = find_top_source(topname, srclist)
|
|
if not top_text:
|
|
issues.append("Top module '{}' was not found in lint source set.".format(topname))
|
|
else:
|
|
for lineno, line in enumerate(top_text.splitlines(), start=1):
|
|
if "add your code" in line.lower():
|
|
issues.append(
|
|
"{}:{}: template placeholder remains in top module; the SoC integration is likely incomplete.".format(
|
|
top_source, lineno
|
|
)
|
|
)
|
|
|
|
module_sources, module_bodies = extract_module_definitions(srclist)
|
|
reachable_modules = find_reachable_modules(topname, module_bodies)
|
|
unreachable_modules = sorted(set(module_bodies) - reachable_modules)
|
|
if len(module_bodies) >= 20 and len(unreachable_modules) > len(module_bodies) // 2:
|
|
shown_modules = ", ".join(unreachable_modules[:12])
|
|
if len(unreachable_modules) > 12:
|
|
shown_modules += ", ..."
|
|
issues.append(
|
|
"{}: only {} of {} project modules are reachable from top '{}'; unreachable modules include: {}.".format(
|
|
top_source,
|
|
len(reachable_modules & set(module_bodies)),
|
|
len(module_bodies),
|
|
topname,
|
|
shown_modules,
|
|
)
|
|
)
|
|
|
|
if issues:
|
|
sanity_log = prjdir / "linter-sanity.log"
|
|
sanity_log.write_text("\n".join(issues) + "\n", encoding="utf-8")
|
|
print("HDL sanity check failed.")
|
|
for issue in issues:
|
|
print(issue)
|
|
print("Full log:", sanity_log)
|
|
sys.exit(1)
|
|
|
|
def run_linter(prjdir: Path, topname: str, headerlist: Set[str], srclist: Set[str], inclist: Set[str]):
|
|
linter_log = prjdir / "linter.log"
|
|
linter_args = prjdir / "linter.args"
|
|
verilator = os.environ.get("VERILATOR") or shutil.which("verilator") or shutil.which("verilator_bin.exe")
|
|
if not verilator:
|
|
print("ERROR: verilator was not found in PATH.", file=sys.stderr)
|
|
print("Install verilator in the CI image, set VERILATOR, or remove the HDL lint step from .gitlab-ci.yml.", file=sys.stderr)
|
|
sys.exit(127)
|
|
|
|
env = os.environ.copy()
|
|
if os.name == "nt" and "VERILATOR_ROOT" not in env:
|
|
verilator_root = Path(verilator).resolve().parent.parent / "share" / "verilator"
|
|
if verilator_root.is_dir():
|
|
env["VERILATOR_ROOT"] = str(verilator_root)
|
|
|
|
timescale_warning = None
|
|
for warning in ("TIMESCALEMOD", "TIMESCALE"):
|
|
if verilator_supports_warning(verilator, env, warning):
|
|
timescale_warning = warning
|
|
break
|
|
|
|
args = [
|
|
verilator,
|
|
"--lint-only",
|
|
"-Wall",
|
|
"-Wno-fatal",
|
|
"-Wno-DECLFILENAME",
|
|
"-Wno-PINCONNECTEMPTY",
|
|
"-Wno-UNUSED",
|
|
"-GSIMULATION=1",
|
|
]
|
|
if timescale_warning:
|
|
args.append("-Wno-" + timescale_warning)
|
|
args += ['--top-module', topname]
|
|
incargs = [ '-I' + i for i in inclist]
|
|
args += incargs
|
|
args += sorted(headerlist)
|
|
args += sorted(srclist)
|
|
|
|
if os.name == "nt":
|
|
response_args = [str(arg).replace("\\", "/") for arg in args[1:]]
|
|
linter_args.write_text("\n".join(response_args) + "\n", encoding="utf-8")
|
|
run_args = [verilator, "-f", str(linter_args)]
|
|
else:
|
|
run_args = args
|
|
|
|
with linter_log.open("w", encoding="utf-8") as log:
|
|
res = subprocess.run(run_args, stdout=log, stderr=subprocess.STDOUT, env=env)
|
|
if res.returncode != 0:
|
|
log_text = linter_log.read_text(encoding="utf-8", errors="replace")
|
|
error_lines = [
|
|
line for line in log_text.splitlines()
|
|
if "%Error" in line or "Error:" in line or "ERROR:" in line
|
|
]
|
|
print("HDL lint failed.")
|
|
if error_lines:
|
|
print("Key errors:")
|
|
for line in error_lines[:20]:
|
|
print(line)
|
|
if len(error_lines) > 20:
|
|
print("... {} more error lines omitted".format(len(error_lines) - 20))
|
|
else:
|
|
print("No explicit error line found in Verilator output.")
|
|
print("Return code:", res.returncode)
|
|
print("Full log:", linter_log)
|
|
sys.exit(res.returncode)
|
|
print("HDL lint passed.")
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
if len(sys.argv) != 2:
|
|
usage()
|
|
sys.exit(2)
|
|
xpr = Path(sys.argv[1])
|
|
if not xpr.is_file():
|
|
print("Vivado project file does not exist:", xpr, file=sys.stderr)
|
|
sys.exit(2)
|
|
topname, headerlist, srclist, inclist = parse_project(xpr)
|
|
if not topname:
|
|
print("Top module was not found in project:", xpr, file=sys.stderr)
|
|
sys.exit(1)
|
|
if not srclist:
|
|
print("No Verilog source files were found in project:", xpr, file=sys.stderr)
|
|
sys.exit(1)
|
|
run_design_sanity_checks(xpr.parent, topname, srclist)
|
|
run_linter(xpr.parent, topname, headerlist, srclist, inclist)
|
|
except Exception:
|
|
traceback.print_exc()
|
|
sys.exit(1)
|