From bbcf542e4cd1c73dfc7dfe4912af47287378a2b1 Mon Sep 17 00:00:00 2001 From: Zapta Date: Sat, 31 Aug 2024 19:55:48 -0700 Subject: [PATCH] This change allows users to define custom resource files by placing a resource file of the same name in their project directory. Currently it is enabled only for boards.json and fpga.json but can easily be enabled for other resources. This way, users can use their own custom board without having to burden the stock resource files with niche boards. The bulk of the change here is making the project dir known in resources.py when the resoruces are loaded. If a custom resource file is used, an info message is printed to facilitate in supporting users. --- DEVELOPER.md | 44 +++++++++++++++++++++++++++++ apio/__main__.py | 2 +- apio/commands/boards.py | 12 +++++++- apio/commands/init.py | 10 +++---- apio/commands/install.py | 30 ++++++++++++++------ apio/commands/system.py | 17 +++++++++++- apio/commands/uninstall.py | 26 +++++++++++++---- apio/managers/arguments.py | 30 ++++++++++---------- apio/managers/drivers.py | 2 +- apio/managers/examples.py | 8 ++++-- apio/managers/installer.py | 32 +++++++++++++-------- apio/managers/project.py | 39 ++++++++++++-------------- apio/managers/scons.py | 57 ++++++++++++++++++++++---------------- apio/managers/system.py | 6 +--- apio/resources.py | 46 ++++++++++++++++++++++++------ apio/util.py | 35 +++++++++++------------ 16 files changed, 268 insertions(+), 128 deletions(-) create mode 100644 DEVELOPER.md diff --git a/DEVELOPER.md b/DEVELOPER.md new file mode 100644 index 00000000..81e5fec0 --- /dev/null +++ b/DEVELOPER.md @@ -0,0 +1,44 @@ +# APIO Developers Hints + +This file is not intended for APIO users. + +## Pre commit tests +Before submitting a new commit, make sure the following commands runs successfuly (in the repository root): + +```shell +make lint +make tox +``` + +## Running an individual APIO test + +Run from the repo root. Replace with the path to the desire test. + +```shell +test/code_commands/test_build.py +``` + +## Running APIO in a debugger + +Set the debugger to run the ``apio_run.py`` main with the regular ``apio`` arguments. Set the project directory ot the project file or use the ``--project_dir`` apio argument to point to the project directory. + +Example of an equivalent manual command: +``` +python apio_run.py build --project_dir ~/projects/fpga/repo/hdl +``` + +## Running APIO commands using a dev repo + +One way is to link the pip package to the dev repository. Something along these lines. Adjust patches to match your system. The ``pip show`` command shows the directory where the stock pip package is installed. + +NOTE: This make the command ``apio init --scons`` opsolete since the scons files can be edited in the dev repository. + +``` +pip install apio +pip show apio +cd /Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages +mv apio apio.original +ln -s ~/projects/apio_dev/repo/apio +``` + + diff --git a/apio/__main__.py b/apio/__main__.py index fec17928..527f300e 100644 --- a/apio/__main__.py +++ b/apio/__main__.py @@ -29,7 +29,7 @@ def __init__(self, *args, **kwargs): # -- Ex. /home/obijuan/Develop/(...)/apio/commands # -- Every apio command (Ex. apio build, apio upload...) is a # -- separate .py file located in the commands folder - self.commands_folder = util.get_full_path("commands") + self.commands_folder = util.get_apio_full_path("commands") self._cls = [None] super().__init__(*args, **kwargs) diff --git a/apio/commands/boards.py b/apio/commands/boards.py index ca10a6b4..636f3e7c 100644 --- a/apio/commands/boards.py +++ b/apio/commands/boards.py @@ -7,6 +7,7 @@ # -- Licence GPLv2 """Main implementation of APIO BOARDS command""" +from pathlib import Path import click from apio.resources import Resources from apio import util @@ -15,12 +16,20 @@ # -- CONSTANTS # ------------------ CMD = "boards" # -- Comand name +PROJECT_DIR = "project_dir" # -- Option LIST = "list" # -- Option FPGA = "fpga" # -- Option @click.command(CMD, context_settings=util.context_settings()) @click.pass_context +@click.option( + "-p", + "--project-dir", + type=Path, + metavar="str", + help="Set the target directory for the project.", +) @click.option( "-l", f"--{LIST}", @@ -34,11 +43,12 @@ def cli(ctx, **kwargs): """Manage FPGA boards.""" # -- Extract the arguments + project_dir = kwargs[PROJECT_DIR] # -- str _list = kwargs[LIST] # -- bool fpga = kwargs[FPGA] # -- bool # -- Access to the apio resources - resources = Resources() + resources = Resources(project_dir=project_dir) # -- Option 1: List boards if _list: diff --git a/apio/commands/init.py b/apio/commands/init.py index aacdb8aa..7abaa4ca 100644 --- a/apio/commands/init.py +++ b/apio/commands/init.py @@ -67,26 +67,26 @@ def cli(ctx, **kwargs): top_module = kwargs[TOP_MODULE] # -- Create a project - project = Project() + project = Project(project_dir) # -- scons option: Create default SConstruct file if scons: - project.create_sconstruct(project_dir, "ice40", sayyes) + project.create_sconstruct("ice40", sayyes) - # -- Create the apio.ini file + # -- Create the project file apio.ini elif board: # -- Set the default top_module when creating the ini file if not top_module: top_module = "main" # -- Create the apio.ini file - project.create_ini(board, top_module, project_dir, sayyes) + project.create_ini(board, top_module, sayyes) # -- Add the top_module to the apio.ini file elif top_module: # -- Update the apio.ini file - project.update_ini(top_module, project_dir) + project.update_ini(top_module) # -- No options: show help else: diff --git a/apio/commands/install.py b/apio/commands/install.py index 636761d2..5d565e53 100644 --- a/apio/commands/install.py +++ b/apio/commands/install.py @@ -7,6 +7,7 @@ # -- Licence GPLv2 """Main implementation of APIO INSTALL command""" +from pathlib import Path import click from apio.managers.installer import Installer, list_packages from apio.resources import Resources @@ -16,6 +17,7 @@ # -- CONSTANTS # ------------------ CMD = "install" # -- Comand name +PROJECT_DIR = "project_dir" # -- Option PACKAGES = "packages" # -- Argument ALL = "all" # -- Option LIST = "list" # -- Option @@ -23,7 +25,9 @@ PLATFORM = "platform" # -- Option -def install_packages(packages: list, platform: str, force: bool): +def install_packages( + packages: list, platform: str, resources: Resources, force: bool +): """Install the apio packages passed as a list * INPUTS: - packages: List of packages (Ex. ['examples', 'oss-cad-suite']) @@ -34,15 +38,23 @@ def install_packages(packages: list, platform: str, force: bool): for package in packages: # -- The instalation is performed by the Installer object - inst = Installer(package, platform, force) + modifiers = Installer.Modifiers(force=force, checkversion=True) + installer = Installer(package, platform, resources, modifiers) # -- Install the package! - inst.install() + installer.install() @click.command(CMD, context_settings=util.context_settings()) @click.pass_context @click.argument(PACKAGES, nargs=-1) +@click.option( + "-p", + "--project-dir", + type=Path, + metavar="str", + help="Set the target directory for the project.", +) @click.option("-a", f"--{ALL}", is_flag=True, help="Install all packages.") @click.option( "-l", f"--{LIST}", is_flag=True, help="List all available packages." @@ -64,23 +76,23 @@ def cli(ctx, **kwargs): # -- Extract the arguments packages = kwargs[PACKAGES] # -- tuple + project_dir = kwargs[PROJECT_DIR] # -- str platform = kwargs[PLATFORM] # -- str _all = kwargs[ALL] # -- bool _list = kwargs[LIST] # -- bool force = kwargs[FORCE] # -- bool + # -- Load the resources. + resources = Resources(platform=platform, project_dir=project_dir) + # -- Install the given apio packages if packages: - install_packages(packages, platform, force) + install_packages(packages, platform, resources, force) # -- Install all the available packages (if any) elif _all: - - # -- Get all the resources - resources = Resources(platform) - # -- Install all the available packages for this platform! - install_packages(resources.packages, platform, force) + install_packages(resources.packages, platform, resources, force) # -- List all the packages (installed or not) elif _list: diff --git a/apio/commands/system.py b/apio/commands/system.py index f7d8fdfa..2ad8ca89 100644 --- a/apio/commands/system.py +++ b/apio/commands/system.py @@ -7,15 +7,19 @@ # -- Licence GPLv2 """Main implementation of APIO SYSTEM command""" +from pathlib import Path import click from apio import util from apio.util import get_systype from apio.managers.system import System +from apio.resources import Resources + # ------------------ # -- CONSTANTS # ------------------ CMD = "system" # -- Comand name +PROJECT_DIR = "project_dir" # -- Option LSFTDI = "lsftdi" # -- Option LSUSB = "lsusb" # -- Option LSSERIAL = "lsserial" # -- Option @@ -24,6 +28,13 @@ @click.command(CMD, context_settings=util.context_settings()) @click.pass_context +@click.option( + "-p", + "--project-dir", + type=Path, + metavar="str", + help="Set the target directory for the project.", +) @click.option( f"--{LSFTDI}", is_flag=True, help="List all connected FTDI devices." ) @@ -39,13 +50,17 @@ def cli(ctx, **kwargs): """System tools.""" # -- Extract the arguments + project_dir = kwargs[PROJECT_DIR] lsftdi = kwargs[LSFTDI] lsusb = kwargs[LSUSB] lsserial = kwargs[LSSERIAL] info = kwargs[INFO] + # Load the various resource files. + resources = Resources(project_dir=project_dir) + # -- Create the system object - system = System() + system = System(resources) # -- List all connected ftdi devices if lsftdi: diff --git a/apio/commands/uninstall.py b/apio/commands/uninstall.py index e2f27f0a..043d3a0a 100644 --- a/apio/commands/uninstall.py +++ b/apio/commands/uninstall.py @@ -7,22 +7,26 @@ # -- Licence GPLv2 """Main implementation of APIO UNINSTALL command""" +from pathlib import Path import click from apio.managers.installer import Installer, list_packages from apio.profile import Profile from apio import util +from apio.resources import Resources + # ------------------ # -- CONSTANTS # ------------------ CMD = "uninstall" # -- Comand name +PROJECT_DIR = "project_dir" # -- Option PACKAGES = "packages" # -- Argument ALL = "all" # -- Option LIST = "list" # -- Option PLATFORM = "platform" # -- Option -def _uninstall(packages: list, platform: str): +def _uninstall(packages: list, platform: str, resources: Resources): """Uninstall the given list of packages""" # -- Ask the user for confirmation @@ -32,10 +36,11 @@ def _uninstall(packages: list, platform: str): for package in packages: # -- The uninstalation is performed by the Installer object - inst = Installer(package, platform, checkversion=False) + modifiers = Installer.Modifiers(force=False, checkversion=False) + installer = Installer(package, platform, resources, modifiers) # -- Uninstall the package! - inst.uninstall() + installer.uninstall() # -- User quit! else: @@ -45,6 +50,13 @@ def _uninstall(packages: list, platform: str): @click.command(CMD, context_settings=util.context_settings()) @click.pass_context @click.argument(PACKAGES, nargs=-1) +@click.option( + "-p", + "--project-dir", + type=Path, + metavar="str", + help="Set the target directory for the project.", +) @click.option("-a", f"--{ALL}", is_flag=True, help="Uninstall all packages.") @click.option( "-l", f"--{LIST}", is_flag=True, help="List all installed packages." @@ -66,10 +78,14 @@ def cli(ctx, **kwargs): platform = kwargs[PLATFORM] # -- str _all = kwargs[ALL] # -- bool _list = kwargs[LIST] # -- bool + project_dir = kwargs[PROJECT_DIR] # -- str + + # -- Load the resources. + resources = Resources(platform=platform, project_dir=project_dir) # -- Uninstall the given apio packages if packages: - _uninstall(packages, platform) + _uninstall(packages, platform, resources) # -- Uninstall all the packages elif _all: @@ -78,7 +94,7 @@ def cli(ctx, **kwargs): packages = Profile().packages # -- Uninstall them! - _uninstall(packages, platform) + _uninstall(packages, platform, resources) # -- List all the packages (installed or not) elif _list: diff --git a/apio/managers/arguments.py b/apio/managers/arguments.py index aec1826b..c69fee8e 100644 --- a/apio/managers/arguments.py +++ b/apio/managers/arguments.py @@ -84,7 +84,7 @@ def outer(*args): # pylint: disable=R0912 # @debug_params def process_arguments( - config_ini: dict, resources: type[Resources] + config_ini: dict, resources: type[Resources], project: type[Project] ) -> tuple: # noqa """Get the final CONFIGURATION, depending on the board and arguments passed in the command line. @@ -102,6 +102,7 @@ def process_arguments( 'top-module`: str //-- Top module name } * Resources: Object for accessing the apio resources + * Project: the contentn of apio.ini, possibly empty if does not exist. * OUTPUT: * Return a tuple (flags, board, arch) - flags: A list of strings with the flags valures: @@ -129,10 +130,6 @@ def process_arguments( # -- Merge the initial configuration to the current configuration config.update(config_ini) - # -- Read the apio project file (apio.ini) - proj = Project() - proj.read() - # -- proj.board: # -- * None: No apio.ini file # -- * "name": Board name (str) @@ -151,18 +148,18 @@ def process_arguments( # -- If there is a project file (apio.ini) the board # -- given by command line overrides it # -- (command line has the highest priority) - if proj.board: + if project.board: # -- As the command line has more priority, and the board # -- given in args is different than the one in the project, # -- inform the user - if config[BOARD] != proj.board: + if config[BOARD] != project.board: click.secho("Info: ignore apio.ini board", fg="yellow") # -- Board name given in the project file else: # -- ...read it from the apio.ini file - config[BOARD] = proj.board + config[BOARD] = project.board # -- The board is given (either by arguments or by project file) if config[BOARD]: @@ -214,8 +211,8 @@ def process_arguments( # -- Priority 2: Use the top module in the apio.ini file # -- if it exists... - if proj.top_module: - config[TOP_MODULE] = proj.top_module + if project.top_module: + config[TOP_MODULE] = project.top_module # -- NO top-module specified!! Warn the user else: @@ -358,11 +355,14 @@ def perror_insuficient_arguments(): fg="red", ) click.secho( - "You have two options:\n" - " 1) Execute your command with\n" - " `--board `\n" - " 2) Create an ini file using\n" - " `apio init --board `", + "You have a few options:\n" + " 1) Change to a project directory with an apio.ini file\n" + " 2) Specify the directory of a project with an apio.ini file\n" + " `--project-dir \n" + " 3) Create a project file apio.ini manually or using\n" + " `apio init --board `\n" + " 4) Execute your command with the flag\n" + " `--board `", fg="yellow", ) diff --git a/apio/managers/drivers.py b/apio/managers/drivers.py index 107a66a6..b49fa22a 100644 --- a/apio/managers/drivers.py +++ b/apio/managers/drivers.py @@ -64,7 +64,7 @@ class Drivers: # -- to the /etc/udev/rules.d folder # -- FTDI source rules file paths - resources = util.get_full_path("resources") + resources = util.get_apio_full_path("resources") ftdi_rules_local_path = resources / "80-fpga-ftdi.rules" # -- Target rule file diff --git a/apio/managers/examples.py b/apio/managers/examples.py index 4016b303..c6cf040d 100644 --- a/apio/managers/examples.py +++ b/apio/managers/examples.py @@ -21,7 +21,7 @@ EXAMPLE_OF_USE_CAD = """ Example of use: - apio examples -f leds + apio examples -f icezum/leds Copy the leds example files to the current directory\n""" EXAMPLE_DIR_FILE = """ @@ -154,7 +154,7 @@ def copy_example_dir(self, example: str, project_dir: Path, sayno: bool): return 1 # -- Get the working dir (current or given) - project_dir = util.check_dir(project_dir) + project_dir = util.get_project_dir(project_dir, create_if_missing=True) # -- Build the destination example path dst_example_path = project_dir / example @@ -218,7 +218,9 @@ def copy_example_files(self, example: str, project_dir: Path, sayno: bool): return 1 # -- Get the working dir (current or given) - dst_example_path = util.check_dir(project_dir) + dst_example_path = util.get_project_dir( + project_dir, create_if_missing=True + ) # -- Build the source example path (where the example was installed) src_example_path = self.examples_dir / example diff --git a/apio/managers/installer.py b/apio/managers/installer.py index a5c6fd06..ef3c41f3 100644 --- a/apio/managers/installer.py +++ b/apio/managers/installer.py @@ -9,6 +9,7 @@ import shutil from pathlib import Path +from dataclasses import dataclass import click import requests @@ -27,8 +28,19 @@ class Installer: """Installer. Class with methods for installing and managing apio packages""" + @dataclass(frozen=True) + class Modifiers: + """A workaround for the linter limitation of 4 arguments per method.""" + + force: bool + checkversion: bool + def __init__( - self, package: str, platform: str = "", force=False, checkversion=True + self, + package: str, + platform: str = "", + resources=None, + modifiers=Modifiers(force=False, checkversion=True), ): """Class initialization. Parameters: * package: Package name to manage/install. It can have a sufix with @@ -44,7 +56,7 @@ def __init__( self.version = None self.force_install = None self.packages_dir = None - self.resources = None + self.resources = resources self.profile = None self.spec_version = None self.package_name = None @@ -65,17 +77,12 @@ def __init__( # -- Attribute Installer.force_install # -- Force installation or not - self.force_install = force + self.force_install = modifiers.force # -- Installer.package_dir: path were the packages are stored # -- Ex. /home/obijuan/.apio/packages self.packages_dir = "" - # -- Get all the resources for the given platform - # -- Some resources depend on the platform (like the packages) - # -- but some others don't (like the boards) - self.resources = Resources(platform) - # -- Read the profile file self.profile = Profile() @@ -108,7 +115,7 @@ def __init__( # Check if the version is ok (It is only done if the # checkversion flag has been activated) - if checkversion: + if modifiers.checkversion: # Check version. The filename is read from the # repostiroy # -- Get the url of the version.txt file @@ -141,7 +148,10 @@ def __init__( ] # -- The package is kwnown but the version is not correct else: - if self.package in self.profile.packages and checkversion is False: + if ( + self.package in self.profile.packages + and modifiers.checkversion is False + ): self.packages_dir = util.get_home_dir() / dirname self.package_name = "toolchain-" + package @@ -499,7 +509,7 @@ def list_packages(platform: str): """List all the available packages""" # -- Get all the resources - resources = Resources(platform) + resources = Resources(platform=platform) # -- List the packages resources.list_packages() diff --git a/apio/managers/project.py b/apio/managers/project.py index bfe3b3e6..1b848621 100644 --- a/apio/managers/project.py +++ b/apio/managers/project.py @@ -31,21 +31,20 @@ class Project: """Class for managing apio projects""" - def __init__(self): + def __init__(self, project_dir: Path): # TODO(zapta): Make these __private and provide getter methods. + self.project_dir = util.get_project_dir(project_dir) self.board: str = None self.top_module: str = None self.native_exe_mode: bool = None - def create_sconstruct(self, project_dir: Path, arch=None, sayyes=False): + def create_sconstruct(self, arch=None, sayyes=False): """Creates a default SConstruct file""" - project_dir = util.check_dir(project_dir) - sconstruct_name = "SConstruct" - sconstruct_path = project_dir / sconstruct_name + sconstruct_path = self.project_dir / sconstruct_name local_sconstruct_path = ( - util.get_full_path("resources") / arch / sconstruct_name + util.get_apio_full_path("resources") / arch / sconstruct_name ) if sconstruct_path.exists(): @@ -76,13 +75,11 @@ def create_sconstruct(self, project_dir: Path, arch=None, sayyes=False): sconstruct_name, sconstruct_path, local_sconstruct_path ) - def create_ini(self, board, top_module, project_dir="", sayyes=False): + def create_ini(self, board, top_module, sayyes=False): """Creates a new apio project file""" - project_dir = util.check_dir(project_dir) - # -- Build the filename - ini_path = project_dir / PROJECT_FILENAME + ini_path = self.project_dir / PROJECT_FILENAME # Check board boards = Resources().boards @@ -112,13 +109,11 @@ def create_ini(self, board, top_module, project_dir="", sayyes=False): self._create_ini_file(board, top_module, ini_path, PROJECT_FILENAME) # TODO- Deprecate prgramatic mutations of apio.ini - def update_ini(self, top_module, project_dir): + def update_ini(self, top_module): """Update the current init file with the given top-module""" - project_dir = util.check_dir(project_dir) - # -- Build the filename - ini_path = project_dir / PROJECT_FILENAME + ini_path = self.project_dir / PROJECT_FILENAME # -- Check if the apio.ini file exists if not ini_path.is_file(): @@ -187,19 +182,21 @@ def _copy_sconstruct_file( def read(self): """Read the project config file""" - # -- If no project finel found, just return - if not isfile(PROJECT_FILENAME): + project_file = self.project_dir / PROJECT_FILENAME + + # -- If no project file found, just return + if not isfile(project_file): print(f"Info: No {PROJECT_FILENAME} file") return # Load the project file. config_parser = ConfigParser() - config_parser.read(PROJECT_FILENAME) + config_parser.read(project_file) for section in config_parser.sections(): if section != "env": message = ( - f"Project file {PROJECT_FILENAME} " + f"Project file {project_file} " f"has an invalid section named " f"[{section}]." ) @@ -208,7 +205,7 @@ def read(self): if "env" not in config_parser.sections(): message = ( - f"Project file {PROJECT_FILENAME}" + f"Project file {project_file}" f"does not have an [env] section." ) print(message) @@ -220,7 +217,7 @@ def read(self): self.top_module = self._parse_top_module( config_parser, parsed_attributes ) - exe_mode = self._parse_exe_mode(config_parser, parsed_attributes) + exe_mode = self._parse_exe_mode(config_parser, parsed_attributes) self.native_exe_mode = {"default": False, "native": True}[exe_mode] # Verify that the project file (api.ini) doesn't contain additional @@ -228,7 +225,7 @@ def read(self): for attribute in config_parser.options("env"): if attribute not in parsed_attributes: message = ( - f"Project file {PROJECT_FILENAME} contains" + f"Project file {project_file} contains" f" an unknown attribute '{attribute}'." ) print(message) diff --git a/apio/managers/scons.py b/apio/managers/scons.py index 5757b3ff..be6b0084 100644 --- a/apio/managers/scons.py +++ b/apio/managers/scons.py @@ -50,20 +50,22 @@ def __init__(self, project_dir: Path): """ # -- Read the project file (apio.ini) - self.proj = Project() - self.proj.read() + self.project = Project(project_dir) + self.project.read() # -- Read the apio profile file self.profile = Profile() # -- Read the apio resources - self.resources = Resources() + self.resources = Resources(project_dir=project_dir) # -- Project path is given if project_dir: # Check if it is a correct folder # (or create a new one) - project_dir = util.check_dir(project_dir) + project_dir = util.get_project_dir( + project_dir, create_if_missing=False + ) # Change to that folder os.chdir(project_dir) @@ -73,7 +75,7 @@ def clean(self, args): """Execute apio clean""" # -- Split the arguments - __, __, arch = process_arguments(args, self.resources) + __, __, arch = process_arguments(args, self.resources, self.project) # --Clean the project: run scons -c (with aditional arguments) return self.run("-c", arch=arch, variables=[], packages=[]) @@ -83,7 +85,7 @@ def verify(self, args): """Executes scons for verifying""" # -- Split the arguments - __, __, arch = process_arguments(args, self.resources) + __, __, arch = process_arguments(args, self.resources, self.project) # -- Execute scons!!! # -- The packages to check are passed @@ -99,7 +101,7 @@ def graph(self, args): """Executes scons for visual graph generation""" # -- Split the arguments - var, _, arch = process_arguments(args, self.resources) + var, _, arch = process_arguments(args, self.resources, self.project) # -- Execute scons!!! # -- The packages to check are passed @@ -115,7 +117,7 @@ def lint(self, args): """DOC: TODO""" config = {} - __, __, arch = process_arguments(config, self.resources) + __, __, arch = process_arguments(config, self.resources, self.project) var = serialize_scons_flags( { "all": args.get("all"), @@ -137,7 +139,7 @@ def sim(self, args): """Simulates a testbench and shows the result in a gtkwave window.""" # -- Split the arguments - var, _, arch = process_arguments(args, self.resources) + var, _, arch = process_arguments(args, self.resources, self.project) return self.run( "sim", @@ -151,7 +153,7 @@ def test(self, args): """Tests all or a single testbench by simulating.""" # -- Split the arguments - var, _, arch = process_arguments(args, self.resources) + var, _, arch = process_arguments(args, self.resources, self.project) return self.run( "test", @@ -165,7 +167,9 @@ def build(self, args): """Build the circuit""" # -- Split the arguments - var, board, arch = process_arguments(args, self.resources) + var, board, arch = process_arguments( + args, self.resources, self.project + ) # -- Execute scons!!! # -- The packages to check are passed @@ -183,7 +187,9 @@ def build(self, args): def time(self, args): """DOC: TODO""" - var, board, arch = process_arguments(args, self.resources) + var, board, arch = process_arguments( + args, self.resources, self.project + ) return self.run( "time", variables=var, @@ -210,7 +216,9 @@ def upload(self, config: dict, prog: dict): # -- Get important information from the configuration # -- It will raise an exception if it cannot be solved - flags, board, arch = process_arguments(config, self.resources) + flags, board, arch = process_arguments( + config, self.resources, self.project + ) # -- Information about the FPGA is ok! @@ -549,8 +557,7 @@ def serialize_programmer( return programmer - @staticmethod - def check_usb(board: str, board_data: dict) -> None: + def check_usb(self, board: str, board_data: dict) -> None: """Check if the given board is connected or not to the computer If it is not connected, an exception is raised @@ -579,7 +586,7 @@ def check_usb(board: str, board_data: dict) -> None: # -- Get the list of the connected USB devices # -- (execute the command "lsusb" from the apio System module) - system = System() + system = System(self.resources) connected_devices = system.get_usb_devices() # -- Check if the given device (vid:pid) is connected! @@ -795,8 +802,9 @@ def get_ftdi_id(self, board, board_data, ext_ftdi_id) -> str: # -- Ex: '0' return ftdi_id - @staticmethod - def _check_ftdi(board: str, board_data: dict, ext_ftdi_id: str) -> str: + def _check_ftdi( + self, board: str, board_data: dict, ext_ftdi_id: str + ) -> str: """Check if the given ftdi board is connected or not to the computer and return its FTDI index @@ -835,7 +843,7 @@ def _check_ftdi(board: str, board_data: dict, ext_ftdi_id: str) -> str: # -- Get the list of the connected FTDI devices # -- (execute the command "lsftdi" from the apio System module) - system = System() + system = System(self.resources) connected_devices = system.get_ftdi_devices() # -- No FTDI devices detected --> Error! @@ -878,7 +886,7 @@ def run(self, command, variables, packages, board=None, arch=None): # -- apio, which is located in the resources/arch/ folder if not scon_file.exists(): # -- This is the default SConstruct file - resources = util.get_full_path("resources") + resources = util.get_apio_full_path("resources") default_scons_file = resources / arch / "SConstruct" # -- It is passed to scons using the flag -f default_scons_file @@ -890,9 +898,11 @@ def run(self, command, variables, packages, board=None, arch=None): # -- Verify necessary packages if needed. # TODO(zapta): Can we drop the 'native' mode for simplicity? - if self.proj.native_exe_mode: - # Assuming blindly that the binaries we need are on the path. - click.secho("Warning: native exe mode (binaries should be on path)") + if self.project.native_exe_mode: + # Assuming blindly that the binaries we need are on the path. + click.secho( + "Warning: native exe mode (binaries should be on path)" + ) else: # Run on `default` config mode # -- Check if the necessary packages are installed @@ -903,7 +913,6 @@ def run(self, command, variables, packages, board=None, arch=None): ): # Exit if a package is not installed raise AttributeError("Package not installed") - # -- Execute scons return self._execute_scons(command, variables, board) diff --git a/apio/managers/system.py b/apio/managers/system.py index d1640602..91cfb847 100644 --- a/apio/managers/system.py +++ b/apio/managers/system.py @@ -12,19 +12,15 @@ from apio import util from apio.profile import Profile -from apio.resources import Resources class System: # pragma: no cover """System class. Managing and execution of the system commands""" - def __init__(self): + def __init__(self, resources: dict): # -- Read the profile from the file profile = Profile() - # -- Read the resources from the corresponding files - resources = Resources() - # -- This command is called system self.name = "system" diff --git a/apio/resources.py b/apio/resources.py index 32c7eab4..fcc1fe96 100644 --- a/apio/resources.py +++ b/apio/resources.py @@ -10,10 +10,13 @@ import json from collections import OrderedDict import shutil +from pathlib import Path +from typing import Optional import click from apio import util from apio.profile import Profile + # -- Info message BOARDS_MSG = ( """ @@ -62,19 +65,26 @@ class Resources: """Resource manager. Class for accesing to all the resources""" - def __init__(self, platform: str = ""): + def __init__( + self, *, platform: str = "", project_dir: Optional[Path] = None + ): + project_dir = util.get_project_dir(project_dir) + + self._project_dir = project_dir # -- Read the apio packages information self.packages = self._load_resource(PACKAGES_JSON) # -- Read the boards information - self.boards = self._load_resource(BOARDS_JSON) + self.boards = self._load_resource(BOARDS_JSON, allow_custom=True) # -- Read the FPGAs information - self.fpgas = self._load_resource(FPGAS_JSON) + self.fpgas = self._load_resource(FPGAS_JSON, allow_custom=True) # -- Read the programmers information - self.programmers = self._load_resource(PROGRAMMERS_JSON) + self.programmers = self._load_resource( + PROGRAMMERS_JSON, allow_custom=True + ) # -- Read the distribution information self.distribution = self._load_resource(DISTRIBUTION_JSON) @@ -98,8 +108,7 @@ def __init__(self, platform: str = ""): # -- Default profile file self.profile = None - @staticmethod - def _load_resource(name: str) -> dict: + def _load_resource(self, name: str, allow_custom: bool = False) -> dict: """Load the resources from a given json file * INPUTS: * Name: Name of the json file @@ -109,12 +118,31 @@ def _load_resource(name: str) -> dict: * FPGAS_JSON * PROGRAMMERS_JSON * DISTRIBUTION_JSON - * OUTPUT: The dicctionary with the data + * Allow_custom: if true, look first in the project dir for + a project specific resource file of same name. + * OUTPUT: A dictionary with the json file data In case of error it raises an exception and finish """ + # -- Try loading a custom resource file from the project directory. + filepath = self._project_dir / name + + if filepath.exists(): + if allow_custom: + click.secho( + f"Loading custom {name} from project dir", fg="yellow" + ) + return self._load_resource_file(filepath) - # -- Build the filepath: Ex. resources/fpgas.json - filepath = util.get_full_path(RESOURCES_DIR) / name + # -- Load the stock resource file from the APIO package. + filepath = util.get_apio_full_path(RESOURCES_DIR) / name + return self._load_resource_file(filepath) + + @staticmethod + def _load_resource_file(filepath: Path) -> dict: + """Load the resources from a given json file path + * OUTPUT: A dictionary with the json file data + In case of error it raises an exception and finish + """ # -- Read the json file try: diff --git a/apio/util.py b/apio/util.py index 76e303e6..07ed7a9d 100644 --- a/apio/util.py +++ b/apio/util.py @@ -99,8 +99,8 @@ def close(self): self.join() -def get_full_path(folder: str) -> Path: - """Get the full path to the given folder +def get_apio_full_path(folder: str) -> Path: + """Get the full path to the given folder in the apio package. Inputs: * folder: String with the folder name @@ -693,39 +693,40 @@ def print_exception_developers(e): click.secho(f"{e}\n", fg="yellow") -def check_dir(_dir: Path) -> Path: +def get_project_dir(_dir: Path, create_if_missing: bool = False) -> Path: """Check if the given path is a folder. It it does not exists - the folder is created. If no path is given the current working - directory is used + and create_if_missing is true, folder is created, otherwise a fatal error. + If no path is given the current working directory is used. * INPUTS: - * _dir: The Path to check + * _dir: The Path to check. * OUTPUT: - * The new path (if not given) + * The effective path (same if given) """ - # -- If no path is given, get the current working directory if not _dir: _dir = Path.cwd() - # -- Check if the path is a file or a folder + # -- Make sure the folder doesn't exist as a file. if _dir.is_file(): - # -- It is a file! Error! Exit! click.secho( f"Error: project directory is already a file: {_dir}", fg="red" ) - sys.exit(1) # -- If the folder does not exist.... if not _dir.exists(): - # -- Warning - click.secho(f"Warning: The path does not exist: {_dir}", fg="yellow") - - # -- Create the folder - click.secho(f"Creating folder: {_dir}") - _dir.mkdir() + if create_if_missing: + click.secho( + f"Warning: The path does not exist: {_dir}", fg="yellow" + ) + click.secho(f"Creating folder: {_dir}") + _dir.mkdir() + else: + click.secho(f"Error: the path does not exist: {_dir}", fg="red") + sys.exit(1) # -- Return the path + # print(f"*** get_project_dir() {_temp} -> {_dir}") return _dir