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