diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..d665aea --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,8 @@ +# Created by venv; see https://docs.python.org/3/library/venv.html +bin +include +lib +lib64 +pyvenv.cfg +__pycache__ +gantry diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..53a3716 --- /dev/null +++ b/src/README.md @@ -0,0 +1,6 @@ +# Installation +* Run `python -m venv .` +* Run `./bin/pip install -r requirements.txt` + +# Running +* `./bin/python gantry.py --help` documents how it works diff --git a/src/build_env.py b/src/build_env.py new file mode 100644 index 0000000..de63805 --- /dev/null +++ b/src/build_env.py @@ -0,0 +1,106 @@ +import os +from re import split +import subprocess +from typing import List + +from project_man import addLibraryInProject, removeLibraryInProject + +def getCfFileId(std: str): + return "08" if std == "08" else "93" ## Weird behaviour from GHDL, but all vhdl versions besides 08 have [...]93.cf + +def ghdlEnvExists(std, lib): + ## Check if work exists + try: + os.lstat("work") + except: + return False + ## Check that work is writable + if not os.access("work", os.W_OK): + print("work is write-protected, please acquire correct permissions") + return False + cfFileExists = False + filesInWork = os.listdir("work") + cfFileId = getCfFileId(std) + for file in filesInWork: + if ".cf" in file and lib in file and cfFileId in file: + cfFileExists = True + if not cfFileExists: + return False + ## Nothing bad, continue + return True + +def addVHDLFiles(fileNames: List[str], std: str, lib: str): + os.makedirs("work", exist_ok=True) + vhdlFiles = [] + if ghdlEnvExists(std=std, lib=lib): + cfFileId = getCfFileId(std) + cfFileName = list(filter(lambda x: ".cf" in x and lib in x and cfFileId in x, os.listdir("work")))[0] + cfFilePath = os.path.join("work",cfFileName) + currentlyAdded = getCurrentlyAddedFiles(cfFilePath) + for fileName in fileNames: + if fileName not in currentlyAdded: + vhdlFiles.append(fileName) + else: + addLibraryInProject(lib, ".", std) + vhdlFiles = fileNames + vhdlFiles = list(filter(lambda x: ".vhd" in x, vhdlFiles)) + if len(vhdlFiles) == 0: + print("no files to add.") + return 0 + print(f"adding {vhdlFiles} to library {lib}") + command = ["ghdl", "-i", "--workdir=work", f"--work={lib}", f"--std={std}"] + vhdlFiles + subprocess.run(command) + return 0 + +def removeVHDLFiles(fileNames: List[str], std: str, lib: str): + if not ghdlEnvExists(std=std, lib=lib): + return -1 + cfFileId = getCfFileId(std) + cfFileName = list(filter(lambda x: ".cf" in x and lib in x and cfFileId in x, os.listdir("work")))[0] + cfFilePath = os.path.join("work",cfFileName) + currentlyAdded = getCurrentlyAddedFiles(cfFilePath) + + for fileName in fileNames: + if fileName not in currentlyAdded: + print(f"file {fileName} is not present in {cfFileName}.") + return 0 + currentlyAdded.remove(fileName) + removeCurrentlyAddedFile(fileName, cfFilePath) + if len(currentlyAdded) == 0: + print("Project is empty, removing GHDL project file and library reference in project") + removeLibraryInProject(lib) + os.remove(cfFilePath) + + +def getCurrentlyAddedFiles(cfFilePath:str): + f = open(cfFilePath,"r") + lines = f.readlines() + f.close() + fileLines = filter(lambda x: "file" in x, lines) + files = map(lambda x: split("\" \"",x)[1], fileLines) + return list(files) + +def removeCurrentlyAddedFile(fileName: str, cfFilePath: str): + f = open(cfFilePath,"r") + lines = f.readlines() + f.close() + try: + mappedLines = list(map(lambda x: fileName in x, lines)) + index = mappedLines.index(True) + endIndex = len(lines)-1 + for x in range(index+1,len(lines)): + if "file" in lines[x]: + endIndex = x + break + newLines = [] + for x in range(len(lines)): + if x < index or x >= endIndex: + newLines.append(lines[x]) + f = open(cfFilePath, "w") + f.writelines(newLines) + f.close() + except: + print(f"Something went wrong when trying to remove {fileName} from {cfFilePath}. Restoring to original configuration") + f = open(cfFilePath, "w") + f.writelines(lines) + f.close() diff --git a/src/dependecies.md b/src/dependecies.md new file mode 100644 index 0000000..0feb6b1 --- /dev/null +++ b/src/dependecies.md @@ -0,0 +1,4 @@ +* GHDL = 4.1.0 +* Python >= 3.0.0 +* gtkwave >= v3.3.120 + diff --git a/src/elab.py b/src/elab.py new file mode 100644 index 0000000..d5be319 --- /dev/null +++ b/src/elab.py @@ -0,0 +1,42 @@ +import os +import subprocess +import build_env +from typing import List +from project_man import getLibrariesPresent, getLibrariesInProject + +def generateIncludesForGHDL(includes: List[str]): + cmd = [] + [exists, projectLibs] = getLibrariesInProject() + if not exists: + return [] + for lib in projectLibs.keys(): + includeString = f"{projectLibs[lib]['path']}/work" + cmd.append(f"-P{includeString}") + for inc in includes: + includeString = f"{inc}/work" + cmd.append(f"-P{includeString}") + return cmd + +def elabDesign(topDef: str, arch: str, lib: str, std: str, includes: List[str]): + if not build_env.ghdlEnvExists(std, lib): + print("No GHDL environment present. Add all needed files before elaborating") + incs = generateIncludesForGHDL(includes) + command = [ + "ghdl", "-m", "--workdir=work", f"--work={lib}", f"--std={std}"] + incs + ["-o", f"work/{topDef}-{arch}", f"work.{topDef}", f"{arch}"] + subprocess.run(command) + +def runDesign(topDef: str, arch: str, lib: str, std: str, includes): + if not build_env.ghdlEnvExists(std, lib): + print("No GHDL environment present. Add all needed files before elaborating") + os.makedirs("wave",exist_ok=True) + wavePath = os.path.join(os.getcwd(), "wave") + incs = generateIncludesForGHDL(includes) + command = [ ## may add -v for verbose + "ghdl", "--elab-run", f"--workdir=work", f"--work={lib}", f"--std={std}"] + incs + ["-o", f"work/{topDef}-{arch}", f"{topDef}", f"{arch}", + f"--wave=wave/{topDef}-{arch}.ghw" ##, "--read-wave-opt= str: + if lib == "": + [exists, presentLibs] = getLibrariesPresent() + if not exists: + print("No libs found.") + return "work" + if len(presentLibs.keys()) == 1: + return presentLibs.popitem()[0] + else: + libs = list(map(lambda x: x[0], presentLibs.items())) + libsText = list(map(lambda i: f"{i}" + ": \"" + libs[i] + "\"", [x for x in range(len(libs))])) + selectionIndex = 0 + while True: + try: + selectionIndex = int(input("More than one library present, please choose: \n" + "\n".join(libsText) + "\n")) + if selectionIndex < 0 or selectionIndex >=len(libs): + raise ValueError + else: + break + except ValueError: + print("Invalid input!"), + return libs[selectionIndex] + else: + return lib + +def autoDetectStd(library: str, std: str) -> str: + if std == "" or std not in complete_vhdl_ver(): + (exists, libDict) = getLibraryInProject(library) + if not exists: + return "93" + else: + return libDict["vhdl-version"] + else: + return std + +def complete_vhdl_ver(): + return ["87", "93", "93c", "00", "02", "08"] + + +@project.command(help="Creates a project file which allows for further project configuration") +def create( + name: Annotated[str, typer.Argument(help="Name of the project. Has no functional impact")] + ): + print(f"Creating a project at {os.getcwd()}/gantry.toml") + initProjectFile(name) + +@project.command(name="add-lib", help="Initializes a library in the project (non destructive)") +def addLib( + libname: Annotated[str, typer.Argument(help="Name of the library. This is what is used for imports in VHDL.")], + libpath: Annotated[str, typer.Argument(help="Relative path to the library. This tells simulators where to import form.")], + std: Annotated[str, typer.Option(help="Which VHDL standard to use. 87, 93, 93c, 00, 02 or 08", autocompletion=complete_vhdl_ver)] = "93" + ): + print(f"Adding library {libname} to gantry.toml") + addLibraryInProject(libname, libpath, std) + +@project.command(name="remove-lib", help="Removes a library in the project (non destructive)") +def removeLib( + libname: Annotated[str, typer.Argument(help="Name of the library.")] + ): + print(f"Removing library {libname} from gantry.toml") + removeLibraryInProject(libname) + +@software.command(help="Adds files to a library. Automatically updates gantry project file and creates GHDL project files") +def add( + filenames: Annotated[List[str], typer.Argument(help="Which files to add to the library. May be more than one.")], + std: Annotated[str, typer.Option(help="Which VHDL standard to use. 87, 93, 93c, 00, 02 or 08", autocompletion=complete_vhdl_ver)] = "93", + library: Annotated[str, typer.Option("--library", "-l", help="Library to compile to.")] = "" + ): + library = autoDetectLibrary(library) + std = autoDetectStd(library, std) + (exists, _) = findProjectFile() + if exists: + addLibraryInProject(library, ".", std) + return build_env.addVHDLFiles(filenames, std, library) + +@software.command(help="Removes files from a library. Automatically updates gantry project file and creates GHDL project files") +def remove( + filenames: Annotated[List[str], typer.Argument(help="Which files to add to the library. May be more than one.")], + std: Annotated[str, typer.Option(help="Which VHDL standard to use. 87, 93, 93c, 00, 02 or 08", autocompletion=complete_vhdl_ver)] = "93", + library: Annotated[str, typer.Option("--library", "-l", help="Library to compile to.")] = "" + ): + library = autoDetectLibrary(library) + std = autoDetectStd(library, std) + return build_env.removeVHDLFiles(filenames, std, library) + + +@software.command(help="Runs analysis and elaboration on the provided top definition and architecture using GHDL. Automatically adds new files not present in the project") +def elab( + topdef: Annotated[str, typer.Argument(help="Top Definition entity to synthesize")] = "", + arch: Annotated[str, typer.Argument(help="Architecture to synthesize within the top definition provided")] = "", + library: Annotated[str, typer.Option("--library", "-l", help="Library to compile to")] = "", + includes: Annotated[Optional[List[str]], typer.Option("--include", "-i", help="Which libraries to include in compile")] = None, + std: Annotated[str, typer.Option(help="Which VHDL standard to use. 87, 93, 93c, 00, 02 or 08", autocompletion=complete_vhdl_ver)] = "93" + ): + library = autoDetectLibrary(library) + std = autoDetectStd(library, std) + print(f"Elaborating {topdef} with arch {arch} in library {library}. VHDL {std}.") + if includes is not None: + print(f"Including libraries: {includes}") + else: + includes = [] + return elaborate.elabDesign(topdef, arch, library, std, includes) + +@software.command(help="Simulates elaborated design in GHDL and views waves in gtkwave. Automatically runs `gantry elab` on the same top def and arch.") +def run( + topdef: Annotated[str, typer.Argument(help="Top Definition entity to synthesize")] = "", + arch: Annotated[str, typer.Argument(help="Architecture to synthesize within the top definition provided")] = "", + library: Annotated[str, typer.Option("--library", "-l", help="Library to compile to")] = "", + includes: Annotated[Optional[List[str]], typer.Option("--include", "-i", help="Which libraries to include in compile")] = None, + std: Annotated[str, typer.Option(help="Which VHDL standard to use. 87, 93, 93c, 00, 02 or 08", autocompletion=complete_vhdl_ver)] = "93" + ): + library = autoDetectLibrary(library) + std = autoDetectStd(library, std) + print(f"Running (and synthesizing if needed) {topdef} with arch {arch} in library {library}. VHDL {std}") + if includes is not None: + print(f"Including libraries: {includes}") + else: + includes = [] + return elaborate.runDesign(topdef, arch, library, std, includes) + +@hardware.command(help="Synthesizes the provided top level design using NXPython. Make sure you run this with NXPython.") +def synth( + topdef: Annotated[str, typer.Argument(help="Top Definition entity to synthesize")] = "", + library: Annotated[str, typer.Option("--library", "-l", help="Library to compile todefaults to \"\"")] = "" + ): + proc = subprocess.run(["source_and_run.sh", f"{gantry_install_path}/nxp_script.py", "synthDesign", library, topdef]) + +@hardware.command(help="Places the provided top level design using NXPython. Make sure you run this with NXPython.") +def place( + topdef: Annotated[str, typer.Argument(help="Top Definition entity to synthesize")] = "", + library: Annotated[str, typer.Option("--library", "-l", help="Library to compile to. If current directory contains libraries, it will be automatically detected")] = "" + ): + proc = subprocess.run(["source_and_run.sh", f"{gantry_install_path}/nxp_script.py", "placeDesign", library, topdef]) + + +@hardware.command(help="Routes the provided top level design using NXPython. Make sure you run this with NXPython.") +def route( + topdef: Annotated[str, typer.Argument(help="Top Definition entity to synthesize")] = "", + library: Annotated[str, typer.Option("--library", "-l", help="Library to compile to. If current directory contains libraries, it will be automatically detected")] = "" + ): + proc = subprocess.run(["source_and_run.sh", f"{gantry_install_path}/nxp_script.py", "routeDesign", library, topdef]) + +@hardware.command(help="") +def build(): + print("Build!") + +if __name__ == "__main__": + app() diff --git a/src/install_gantry.sh b/src/install_gantry.sh new file mode 100755 index 0000000..f886ba3 --- /dev/null +++ b/src/install_gantry.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +PWD=$(pwd) +ESCAPED_PWD=$(printf '%s\n' "$PWD" | sed -e 's/[\/&]/\\&/g') +echo $ESCAPED_PWD +sed -i "0,/gantry_install_path =\"\"/s/gantry_install_path = \"\"/gantry_install_path = \"$ESCAPED_PWD\"/" gantry.py +printf "#!/bin/bash \n$PWD/bin/python3 $PWD/gantry.py \$@" > "$PWD/gantry" +chmod +x "$PWD/gantry" + diff --git a/src/nxp_script.py b/src/nxp_script.py new file mode 100644 index 0000000..fc310b7 --- /dev/null +++ b/src/nxp_script.py @@ -0,0 +1,81 @@ +import os +import subprocess +import build_env +import sys +import traceback +from nxpython import * + +artifact_path = "" + +# Evaluate whether we want the user to provide a selection of files +def makeProject(library: str, project_name: str, path): + # Create an Impulse project and add all VHDL files to it + print(path) + #Createproject enters the implicitly created directory + print(f"Creating project \'{project_name}\'\n") + getProject().setTopCellName(library, project_name) + getProject().setVariantName("NG-MEDIUM", "LGA-625") + getProject().addParameters({}) + print(path) + listed = list(os.listdir(path)) + files = list(filter(lambda x: ".vhdl" in x or ".vhd" in x or ".v\0" in x, listed)) # Find VHDL and Verilog files + files = list(map(lambda x: os.path.join(path, x), files)) + print(f"Adding the following files to project: {files}\n") + getProject().addFiles(files) + print("Saving project") + getProject().save(os.path.join(artifact_path, project_name + ".nym")) + return 0 + +# Do we want the user to specify how far they want to progress wach step? What does the number mean? +def synthDesign(library: str, project_name: str, path): + # Synthesize the design of the project of the provided `project_name` + print("Starting synthesis\n") + try: + nymFile = os.path.join(artifact_path, project_name + ".nym") + os.lstat(nymFile) + getProject().loadNative(nymFile) + except: + print("No existing project found, creating new project\n") + makeProject(library, project_name, path) + getProject().loadNative(os.path.join(artifact_path, project_name + ".nym")) + getProject().progress("Synthesize", 3) + getProject().save(os.path.join(artifact_path, project_name + "-synth.nym")) + return 0 + +def placeDesign(library: str, project_name: str, path): + # Place the given design. Will use a previously synthesized project if available, otherwise one will be created. + print("Starting place\n") + try: + nymFile = os.path.join(artifact_path, project_name + "-synth.nym") + os.lstat(nymFile) + getProject().load(nymFile) + except: + print("No existing synthesis found, entering synthesis stage\n") + synthDesign(library, project_name, path) + getProject().load(os.path.join(artifact_path, project_name + "-synth.nym")) + getProject().progress("Place", 5) + getProject().save(os.path.join(artifact_path, project_name + "-place.nym")) + +def routeDesign(library: str, project_name: str, path): + # Route the given design. Will use a previously placed project if available, otherwise one will be created. + print("Starting route\n") + try: + nymFile = os.path.join(artifact_path, project_name + "-place.nym") + os.lstat(nymFile) + getProject().load(nymFile) + except: + print("No existing place found, entering place stage\n") + placeDesign(library, project_name, path) + getProject().load(os.path.join(artifact_path, project_name + "-place.nym")) + getProject().progress("Route", 3) + getProject().save(os.path.join(artifact_path, project_name + "-route.nym")) + + +if __name__ == "__main__": + path = os.getcwd() + artifact_path = os.path.join(path, "syn") + print(f"Calling {sys.argv[1]}() with arguments {sys.argv[2]}, {sys.argv[3]}") + createProject(artifact_path) + globals()[sys.argv[1]](sys.argv[2], sys.argv[3], path) + getProject().createAnalyzer() + getProject().getAnalyzer().launch(conditions="worstcase", maximumSlack=0, searchPathsLimit=10, synthesisMode=False) diff --git a/src/project_man.py b/src/project_man.py new file mode 100644 index 0000000..55f062d --- /dev/null +++ b/src/project_man.py @@ -0,0 +1,161 @@ +import os +from typing import Any +import toml +import datetime + + +def findProjectRoot() -> "tuple[bool, str]": + [exists, projectFile] = findProjectFile() + if not exists: + return (False, "") + return (True, "/".join(projectFile.split("/")[0:-1])) + +def getRelativePathToRoot(path: str) -> str: + (exists, projectRoot) = findProjectRoot() + if not exists: + return "" + return os.path.relpath(path, projectRoot) + +def findProjectFile() -> "tuple[bool, str]": + cwd = os.getcwd().split("/") + for i in range(len(cwd) - 2): + searchPath = "/".join(cwd[0:len(cwd)-i]) + [exists, pathToProjectFile] = projectFileExists(searchPath) + if exists: + return (True, pathToProjectFile) + return (False, "") + +def projectFileExists(path: str) -> "tuple[bool, str]": + files = os.listdir(path) + for file in files: + if "gantry.toml" in file: + return (True, os.path.join(path, file)) + return (False,"") + +def initProjectFile(projectName: str) -> "tuple[bool, str]": + cwd = os.getcwd() + projectPath = os.path.join(cwd, "gantry.toml") + [exists, existingProjectPath] = projectFileExists(cwd) + if exists: + existingProjectName = existingProjectPath.split("/")[-1] + return (False, f"Project {existingProjectName} already exists, amend it to fit your intention or delete it to create a new project") + try: + with open(projectPath, "w") as f: + parsedTOML = toml.loads(createProjectFileTemplate(projectName)) + toml.dump(parsedTOML, f) + except: + return (False, "Creation of file failed, permissions may be set wrong") + + return (True, projectPath) + +def loadProjectFile() -> "tuple[bool, str | dict[str, Any]]" : + [exists, path] = findProjectFile() + if not exists: + return (False, "") + try: + with open(path, "r") as f: + toml.load + parsedTOML = toml.load(f) + return (True, parsedTOML) + except: + return (False, "Reading Project file failed, permissions may be set wrong") + +def writeProjectFile(projectDict: "dict[str, Any]") -> "tuple[bool, str]": + [exists, path] = findProjectFile() + if not exists: + return (False, "") + try: + with open(path, "w") as f: + toml.dump(projectDict, f) + return (True, "") + except: + return (False, "Reading Project file failed, permissions may be set wrong") + +def getProjectDict() -> "tuple[bool, dict[str, Any]]": + [exists, output] = loadProjectFile() + if not exists: + print(output) + return (False, {}) + if isinstance(output, dict): + return (True, output) + else: + print(output) + return (False, {}) + +def removeLibraryInProject(lib: str) -> "tuple[bool, str]": + [exists, projectDict] = getProjectDict() + if not exists: + return (False, "Found no project dictionary") + if "libraries" not in projectDict.keys(): + return (False, "No libraries are declared in this project.") + if lib in projectDict["libraries"].keys(): + projectDict["libraries"].pop(lib) + [wentWell, _] = writeProjectFile(projectDict) + return (wentWell, "") + return (False, "Library with this name is not declared") + + +def addLibraryInProject(lib: str, relPath: str, std: str) -> "tuple[bool, str]": + (exists, projectDict) = getProjectDict() + if not exists: + return (False, "Project doesn't exist.") + if "libraries" not in projectDict.keys(): + projectDict["libraries"] = {} + if lib not in projectDict["libraries"].keys(): + libDir = getRelativePathToRoot(os.path.join(os.getcwd(), relPath)) + projectDict["libraries"][lib] = {} + projectDict["libraries"][lib]["vhdl-version"] = std + projectDict["libraries"][lib]["path"] = libDir + os.makedirs(name=relPath,exist_ok=True) + print(libDir) + [wentWell, _] = writeProjectFile(projectDict) + return (wentWell, "") + return (False, "Library with this name is already declared") + +def getLibraryInProject(lib: str) -> "tuple[bool, dict[str, Any]]": + (exists, projectDict) = getProjectDict() + if not exists: + return (False, {}) + libs = {} + if "libraries" not in projectDict.keys(): + ## Successful read, no libs found -> empty return + return (False, {}) + (_, projectRoot) = findProjectRoot() + if lib in projectDict["libraries"].keys(): + return (True, projectDict["libraries"][lib]) + return (False, {}) +def getLibrariesInProject() -> "tuple[bool, dict[str, Any]]": + (exists, projectDict) = getProjectDict() + if not exists: + return (False, {}) + libs = {} + if "libraries" not in projectDict.keys(): + ## Successful read, no libs found -> empty return + return (True, {}) + (_, projectRoot) = findProjectRoot() + for lib in projectDict["libraries"].keys(): + libs[lib] = projectDict["libraries"][lib] + absPath = os.path.join(projectRoot, libs[lib]["path"]) + libs[lib]["path"] = os.path.relpath(absPath, os.getcwd()) + return (True, libs) + +def getLibrariesPresent() -> "tuple[bool, dict[str, Any]]": + cwd = os.getcwd() + (exists, libs) = getLibrariesInProject() + if not exists: + return (False, {}) + libsInCwd = {} + for lib in libs.keys(): + if os.path.samefile(libs[lib]["path"], cwd): + libsInCwd[lib] = libs[lib] + return (True, libsInCwd) + +def createProjectFileTemplate(projectName: str) -> str: + return f""" +title = "{projectName}" +createdAt = "{datetime.date.today()}" +maintainer = "" +email = "" +version = "0.0.1" +""" + diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..5f05862 --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,14 @@ +attrs==21.4.0 +click==8.0.4 +commonmark==0.9.1 +dataclasses==0.8 +importlib-metadata==4.8.3 +markdown-it-py==2.0.1 +mdurl==0.1.0 +Pygments==2.14.0 +rich==12.6.0 +shellingham==1.4.0 +type-extensions==0.1.2 +typer==0.10.0 +typing_extensions==4.1.1 +zipp==3.6.0