diff --git a/scripts/build_env.py b/scripts/build_env.py new file mode 100644 index 0000000..de63805 --- /dev/null +++ b/scripts/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/scripts/elab.py b/scripts/elab.py new file mode 100644 index 0000000..d5be319 --- /dev/null +++ b/scripts/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/scripts/project_man.py b/scripts/project_man.py new file mode 100644 index 0000000..55f062d --- /dev/null +++ b/scripts/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" +""" +