diff --git a/apio/__main__.py b/apio/__main__.py index f1a8a066..c2f07eab 100644 --- a/apio/__main__.py +++ b/apio/__main__.py @@ -45,13 +45,6 @@ "raw", "upgrade", ], - "Deprecated commands": [ - "modify", - "time", - "verify", - "install", - "uninstall", - ], } diff --git a/apio/apio_context.py b/apio/apio_context.py index d4c41e89..d1e7ce4c 100644 --- a/apio/apio_context.py +++ b/apio/apio_context.py @@ -102,16 +102,11 @@ def __init__( # -- Maps the optional project_dir option to a path. self.project_dir: Path = util.get_project_dir(project_dir) - ApioContext._check_no_spaces_in_dir(self.project_dir, "project") # -- Determine apio home dir self.home_dir: Path = ApioContext._get_home_dir() ApioContext._check_no_spaces_in_dir(self.home_dir, "home") - # -- Determine apio home dir - self.packages_dir: Path = ApioContext._get_packages_dir(self.home_dir) - ApioContext._check_no_spaces_in_dir(self.packages_dir, "packages") - # -- Profile information, from ~/.apio/profile.json self.profile = Profile(self.home_dir) @@ -160,15 +155,14 @@ def __init__( sorted(self.fpgas.items(), key=lambda t: t[0]) ) - # -- Save the load_project request, mostly for debugging. - self.project_loading_requested = load_project - - # -- If requested, try to load the project's apio.ini. If apio.ini - # -- does not exist, the loading returns None. - self._project: Project = None + # -- If requested, load apio.ini, fatal error if not found. if load_project: resolver = _ProjectResolverImpl(self) self._project = load_project_from_file(self.project_dir, resolver) + assert self.has_project_loaded, "init(): roject not loaded" + else: + self._project: Project = None + assert not self.has_project_loaded, "init(): project loaded" def lookup_board_id( self, board: str, *, warn: bool = True, strict: bool = True @@ -221,16 +215,21 @@ def _check_no_spaces_in_dir(dir_path: Path, subject: str): contains white space. See https://github.com/FPGAwars/apio/issues/474 """ # -- Match a single white space in the dir path. - # *- if re.search("\\s", str(dir_path)): - # -- Here space found. This is a fatal error since we don't hand - # -- it well later in the process. - # *- click.secho( - # *- f"Error: The apio {subject} directory path contains white " - # *- "space.", - # *- fg="red", - # *- ) - # *- click.secho(f"'{str(dir_path)}'", fg="red") - # *- sys.exit(1) + if re.search("\\s", str(dir_path)): + # -- Here space found. This is a fatal error since we don't hand + # -- it well later in the process. + click.secho( + f"Error: The apio {subject} directory path contains white " + "space.", + fg="red", + ) + click.secho(f"'{str(dir_path)}'", fg="red") + sys.exit(1) + + @property + def packages_dir(self): + """Returns the directory hat contains the installed apio packages.""" + return self.home_dir / "packages" @property def has_project_loaded(self): @@ -239,9 +238,10 @@ def has_project_loaded(self): @property def project(self) -> Project: - """Return the project. It's None if project loading not requested or - project doesn't have apio.ini. - .""" + """Return the project. Should be called only if has_project_loaded() is + True.""" + # -- Failure here is a programming error, not a user error. + assert self.has_project_loaded, "project(): project is not loaded" return self._project def _load_resource(self, name: str, allow_custom: bool = False) -> dict: @@ -424,60 +424,6 @@ def get_package_folder_name(self, package_name: str) -> str: return folder_name - def get_platform_packages_lists(self) -> tuple[list, list]: - """Get all the packages that are applicable to this platform, - grouped as installed and not installed - * OUTPUT: - - A tuple of two lists: Installed and not installed packages - """ - - # -- Classify the packages in two lists - installed_packages = [] - notinstalled_packages = [] - - # -- Go though all the apio packages and add them to the installed - # -- or uninstalled lists. - for package_name, package_config in self.platform_packages.items(): - - # -- Collect information about the package - data = { - "name": package_name, - "version": None, - "description": package_config["description"], - } - - # -- Check if this package is installed - if package_name in self.profile.packages: - - # -- Get the installed version - version = self.profile.packages[package_name]["version"] - - # -- Store the version - data["version"] = version - - # -- Store the package - installed_packages += [data] - - # -- The package is not installed - else: - notinstalled_packages += [data] - - # -- If there are in the profile packages that are not in the - # -- platform packages, add them well to the uninstalled list, as - # -- 'unknown'. These must be some left overs, e.g. if apio is - # -- upgraded. - for package_name in self.profile.packages: - if package_name not in self.platform_packages: - data = { - "name": package_name, - "version": "Unknown", - "description": "Unknown deprecated package", - } - installed_packages += [data] - - # -- Return the packages, classified - return installed_packages, notinstalled_packages - def get_package_dir(self, package_name: str) -> Path: """Returns the root path of a package with given name.""" @@ -605,11 +551,10 @@ def is_windows(self) -> bool: @staticmethod def _get_home_dir() -> Path: """Get the absolute apio home dir. This is the apio folder where the - profle is located and the packages are installed (unless - APIO_PACKAGES_DIR is used). + profle is located and the packages are installed. The apio home dir can be overridden using the APIO_HOME_DIR environment varible or in the /etc/apio.json file (in - Debian). If not set, the user_HOME/.apio folder is used by default: + Debian). If not set, the user_home/.apio folder is used by default: Ej. Linux: /home/obijuan/.apio If the folders does not exist, they are created """ @@ -642,63 +587,6 @@ def _get_home_dir() -> Path: # Return the home_dir as a Path return home_dir - @staticmethod - def _get_packages_dir(home_dir: Path) -> Path: - """Return the base directory of apio packages. - Packages are installed in the following folder: - * Default: $APIO_HOME_DIR/packages - * $APIO_PACKAGES_DIR: if the APIO_PACKAGES_DIR env variable is set - * INPUT: - - pkg_name: Package name (Ex. 'examples') - * OUTPUT: - - The package absolute folder (PosixPath) - (Ex. '/home/obijuan/.apio/packages) - The absolute path of the returned directory is guaranteed to have - the word packages in it. - """ - - # -- Get the APIO_PACKAGES_DIR env variable - # -- It returns None if it was not defined - packaged_dir_override = env_options.get(env_options.APIO_PACKAGES_DIR) - - # -- Handle override. - if packaged_dir_override: - # -- Verify that the override dir contains the word packages in its - # -- absolute path. This is a safety mechanism to prevent - # -- uninentional bulk deletions in unintended directories. We - # -- check it each time before we perform a package deletion. - path = Path(packaged_dir_override).absolute() - if "packages" not in str(path).lower(): - click.secho( - "Error: packages directory path does not contain the word " - f"packages: {str(path)}", - fg="red", - ) - click.secho( - "For safety reasons, if you use the environment variable " - "APIO_PACKAGE_DIR to override\n" - "the packages dir, the new directory must have the word " - "'packages' (case insensitive)\n" - "in its absolute path.", - fg="yellow", - ) - sys.exit(1) - - # -- Override is OK. Use it as the packages dir. - packages_dir = Path(packaged_dir_override) - - # -- Else, use the default value. - else: - # -- Ex '/home/obijuan/.apio/packages/tools-oss-cad-suite' - # -- Guaranteed to be absolute. - packages_dir = home_dir / "packages" - - # -- Sanity check. If this fails, this is a programming error. - assert "packages" in str(packages_dir).lower(), packages_dir - - # -- All done. - return packages_dir - # pylint: disable=too-few-public-methods class _ProjectResolverImpl(ProjectResolver): diff --git a/apio/commands/build.py b/apio/commands/build.py index 8ef7eafe..527925b1 100644 --- a/apio/commands/build.py +++ b/apio/commands/build.py @@ -50,12 +50,6 @@ @options.verbose_option @options.verbose_yosys_option @options.verbose_pnr_option -@options.top_module_option_gen(deprecated=True) -@options.board_option_gen(deprecated=True) -@options.fpga_option_gen(deprecated=True) -@options.size_option_gen(deprecated=True) -@options.type_option_gen(deprecated=True) -@options.pack_option_gen(deprecated=True) def cli( _: click.core.Context, # Options @@ -63,13 +57,6 @@ def cli( verbose: bool, verbose_yosys: bool, verbose_pnr: bool, - # Deprecated options - top_module: str, - board: str, - fpga: str, - size: str, - type_: str, - pack: str, ): """Implements the apio build command. It invokes the toolchain to syntesize the source files into a bitstream file. @@ -90,12 +77,6 @@ def cli( # -- Build the project with the given parameters exit_code = scons.build( { - "board": board, - "fpga": fpga, - "size": size, - "type": type_, - "pack": pack, - "top-module": top_module, "verbose_all": verbose, "verbose_yosys": verbose_yosys, "verbose_pnr": verbose_pnr, diff --git a/apio/commands/clean.py b/apio/commands/clean.py index 4ce28820..56d1ddf7 100644 --- a/apio/commands/clean.py +++ b/apio/commands/clean.py @@ -42,13 +42,10 @@ ) @click.pass_context @options.project_dir_option -@options.board_option_gen(deprecated=True) def cli( _: click.core.Context, # Options project_dir: Path, - # Deprecated options. - board: str, ): """Implements the apio clean command. It deletes temporary files generated by apio commands. @@ -61,7 +58,7 @@ def cli( scons = SCons(apio_ctx) # -- Build the project with the given parameters - exit_code = scons.clean({"board": board}) + exit_code = scons.clean(args={}) # -- Done! sys.exit(exit_code) diff --git a/apio/commands/create.py b/apio/commands/create.py index 590ec625..69134263 100644 --- a/apio/commands/create.py +++ b/apio/commands/create.py @@ -21,6 +21,17 @@ from apio.apio_context import ApioContext +board_option = click.option( + "board", # Var name. + "-b", + "--board", + type=str, + required=True, + metavar="board_id", + help="Set the board.", + cls=cmd_util.ApioOption, +) + # --------------------------- # -- COMMAND # --------------------------- @@ -59,7 +70,7 @@ cls=cmd_util.ApioCommand, ) @click.pass_context -@options.board_option_gen(help="Set the board.", required=True) +@board_option @options.top_module_option_gen(help="Set the top level module name.") @options.project_dir_option @options.sayyes diff --git a/apio/commands/install.py b/apio/commands/install.py deleted file mode 100644 index 9f7709f6..00000000 --- a/apio/commands/install.py +++ /dev/null @@ -1,178 +0,0 @@ -# -*- coding: utf-8 -*- -# -- This file is part of the Apio project -# -- (C) 2016-2024 FPGAwars -# -- Authors -# -- * Jesús Arroyo (2016-2019) -# -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 -"""Implementation of 'apio install' command""" - -import sys -import shutil -from pathlib import Path -from typing import Tuple -from varname import nameof -import click -from apio.managers.old_installer import Installer -from apio.apio_context import ApioContext -from apio import cmd_util -from apio.commands import options - - -# R0801: Similar lines in 2 files -# pylint: disable=R0801 -def _install_packages( - apio_ctx: ApioContext, - packages: list, - force: bool, - verbose: bool, -): - """Install the apio packages passed as a list - * INPUTS: - - packages: List of packages (Ex. ['examples', 'oss-cad-suite']) - - platform: Specific platform (Advanced, just for developers) - - force: Force package installation - - verbose: Show detailed output. - """ - # -- Install packages, one by one... - for package in packages: - - # -- The instalation is performed by the Installer object - modifiers = Installer.Modifiers( - force=force, checkversion=True, verbose=verbose - ) - installer = Installer(package, apio_ctx, modifiers) - - # -- Install the package! - installer.install() - - -def _list_packages(apio_ctx: ApioContext, installed=True, notinstalled=True): - """Print the packages list.""" - - # Classify packages - installed_packages, notinstalled_packages = ( - apio_ctx.get_platform_packages_lists() - ) - - # -- Calculate the terminal width - terminal_width, _ = shutil.get_terminal_size() - - # -- String with a horizontal line with the same width - # -- as the terminal - line = "─" * terminal_width - dline = "═" * terminal_width - - if installed and installed_packages: - - # ------- Print installed packages table - # -- Print the header - click.secho() - click.secho(dline, fg="green") - click.secho("Installed packages:", fg="green") - - for package in installed_packages: - click.secho(line) - name = click.style(f"{package['name']}", fg="cyan", bold=True) - version = package["version"] - description = package["description"] - - click.secho(f"{name} {version}") - click.secho(f" {description}") - - click.secho(dline, fg="green") - click.secho(f"Total: {len(installed_packages)}") - - if notinstalled and notinstalled_packages: - - # ------ Print not installed packages table - # -- Print the header - click.secho() - click.secho(dline, fg="yellow") - click.secho("Available packages (Not installed):", fg="yellow") - - for package in notinstalled_packages: - - click.secho(line) - name = click.style(f"{package['name']}", fg="red") - description = package["description"] - click.secho(f"{name} {description}") - - click.secho(dline, fg="yellow") - click.secho(f"Total: {len(notinstalled_packages)}") - - click.secho("\n") - - -# --------------------------- -# -- COMMAND -# --------------------------- -HELP = """ -The command 'apio install' has been deprecated. Please use the command -'apio packages' command instead. -""" - - -# pylint: disable=duplicate-code -# pylint: disable=too-many-arguments -# pylint: disable=too-many-positional-arguments -@click.command( - "install", - short_help="[Depreciated] Install apio packages.", - help=HELP, - cls=cmd_util.ApioCommand, -) -@click.pass_context -@click.argument("packages", nargs=-1, required=False) -@options.list_option_gen(help="List all available packages.") -@options.all_option_gen(help="Install all packages.") -@options.force_option_gen(help="Force the packages installation.") -@options.project_dir_option -@options.verbose_option -def cli( - cmd_ctx: click.core.Context, - # Arguments - packages: Tuple[str], - # Options - list_: bool, - all_: bool, - force: bool, - project_dir: Path, - verbose: bool, -): - """Implements the install command which allows to - manage the installation of apio packages. - """ - - click.secho( - "The 'apio install' command is deprecated. " - "Please use the 'apio packages' command instead.", - fg="yellow", - ) - - # Make sure these params are exclusive. - cmd_util.check_at_most_one_param(cmd_ctx, nameof(packages, all_, list_)) - - # -- Create the apio context. - apio_ctx = ApioContext(project_dir=project_dir, load_project=False) - - # -- Install the given apio packages - if packages: - _install_packages(apio_ctx, packages, force, verbose) - sys.exit(0) - - # -- Install all the available packages (if any) - if all_: - # -- Install all the available packages for this platform! - _install_packages( - apio_ctx, apio_ctx.platform_packages.keys(), force, verbose - ) - sys.exit(0) - - # -- List all the packages (installed or not) - if list_: - _list_packages(apio_ctx) - sys.exit(0) - - # -- Invalid option. Just show the help - click.secho(cmd_ctx.get_help()) diff --git a/apio/commands/lint.py b/apio/commands/lint.py index 003bd01c..a4da1897 100644 --- a/apio/commands/lint.py +++ b/apio/commands/lint.py @@ -18,6 +18,16 @@ # --------------------------- # -- COMMAND SPECIFIC OPTIONS # --------------------------- + +all_option = click.option( + "all_", # Var name. Deconflicting from Python'g builtin 'all'. + "-a", + "--all", + is_flag=True, + help="Enable all warnings, including code style warnings.", + cls=cmd_util.ApioOption, +) + nostyle_option = click.option( "nostyle", # Var name "--nostyle", @@ -77,9 +87,7 @@ @options.top_module_option_gen( help="Restrict linting to this module and its depedencies." ) -@options.all_option_gen( - help="Enable all warnings, including code style warnings." -) +@all_option @nostyle_option @nowarn_option @warn_option diff --git a/apio/commands/modify.py b/apio/commands/modify.py deleted file mode 100644 index 0d1e59a1..00000000 --- a/apio/commands/modify.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -# -- This file is part of the Apio project -# -- (C) 2016-2024 FPGAwars -# -- Authors -# -- * Jesús Arroyo (2016-2019) -# -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 -"""Implementation of 'apio modify' command""" - -import sys -from pathlib import Path -from varname import nameof -import click -from apio.managers.project import modify_project_file -from apio import cmd_util -from apio.commands import options -from apio.apio_context import ApioContext - - -# --------------------------- -# -- COMMAND -# --------------------------- -HELP = """ -The command 'apio modify' has been deprecated. Please edit the 'apio.ini' -file manually with a text editor. -""" - - -# R0913: Too many arguments (6/5) -# pylint: disable=R0913 -@click.command( - "modify", - short_help="[Depreciated] Modify the apio.ini project file.", - help=HELP, - cls=cmd_util.ApioCommand, -) -@click.pass_context -@options.board_option_gen(help="Set the board.") -@options.top_module_option_gen(help="Set the top level module name.") -@options.project_dir_option -def cli( - cmd_ctx: click.core.Context, - # Options - board: str, - top_module: str, - project_dir: Path, -): - """Modify the project file.""" - - # At least one of these options are required. - cmd_util.check_at_least_one_param(cmd_ctx, nameof(board, top_module)) - - # -- Create the apio context. - apio_ctx = ApioContext(project_dir=project_dir, load_project=False) - - # -- If board is not empty, map it to canonical board id. This - # -- fails if the board is unknown. - if board: - board = apio_ctx.lookup_board_id(board) - - # Create the apio.ini file - ok = modify_project_file( - apio_ctx.project_dir, - board, - top_module, - ) - - exit_code = 0 if ok else 1 - sys.exit(exit_code) diff --git a/apio/commands/options.py b/apio/commands/options.py index 8cf9611f..8cb9b9cf 100644 --- a/apio/commands/options.py +++ b/apio/commands/options.py @@ -25,20 +25,6 @@ # ---------------------------------- -# W0622: Redefining built-in 'help' -# pylint: disable=W0622 -def all_option_gen(*, help: str): - """Generate an --all option with given help text.""" - return click.option( - "all_", # Var name. Deconflicting from Python'g builtin 'all'. - "-a", - "--all", - is_flag=True, - help=help, - cls=cmd_util.ApioOption, - ) - - # W0622: Redefining built-in 'help' # pylint: disable=W0622 def force_option_gen(*, help: str): @@ -67,25 +53,6 @@ def list_option_gen(*, help: str): ) -# W0622: Redefining built-in 'help' -# pylint: disable=W0622 -def board_option_gen( - *, deprecated: bool = False, required=False, help: str = "Set the board." -): - """Generate a --board option with given help text.""" - return click.option( - "board", # Var name. - "-b", - "--board", - type=str, - required=required, - metavar="str", - deprecated=deprecated, - help=help, - cls=cmd_util.ApioOption, - ) - - # W0622: Redefining built-in 'help' # pylint: disable=W0622 def top_module_option_gen( @@ -106,96 +73,11 @@ def top_module_option_gen( ) -# W0622: Redefining built-in 'help' -# pylint: disable=W0622 -def fpga_option_gen( - *, - deprecated: bool = False, - help: str = "Set the FPGA.", -): - """Generate a --fpga option with given help text.""" - return click.option( - "fpga", # Var name. - "--fpga", - type=str, - metavar="str", - deprecated=deprecated, - help=help, - cls=cmd_util.ApioOption, - ) - - -# W0622: Redefining built-in 'help' -# pylint: disable=W0622 -def size_option_gen( - *, - deprecated: bool = False, - help: str = "Set the FPGA size (1k/8k).", -): - """Generate a --size option with given help text.""" - return click.option( - "size", # Var name - "--size", - type=str, - metavar="str", - deprecated=deprecated, - help=help, - cls=cmd_util.ApioOption, - ) - - -# W0622: Redefining built-in 'help' -# pylint: disable=W0622 -def type_option_gen( - *, - deprecated: bool = False, - help: str = "Set the FPGA type (hx/lp).", -): - """Generate a --type option with given help text.""" - return click.option( - "type_", # Var name. Deconflicting from Python's builtin 'type'. - "--type", - type=str, - metavar="str", - deprecated=deprecated, - help=help, - cls=cmd_util.ApioOption, - ) - - -# W0622: Redefining built-in 'help' -# pylint: disable=W0622 -def pack_option_gen( - *, - deprecated: bool = False, - help: str = "Set the FPGA package.", -): - """Generate a --pack option with given help text.""" - return click.option( - "pack", # Var name - "--pack", - type=str, - metavar="str", - deprecated=deprecated, - help=help, - cls=cmd_util.ApioOption, - ) - - # --------------------------- # -- Static options # --------------------------- -ftdi_id = click.option( - "ftdi_id", # Var name. - "--ftdi-id", - type=str, - metavar="ftdi-id", - help="Set the FTDI id.", -) - - project_dir_option = click.option( "project_dir", # Var name. "-p", @@ -225,15 +107,6 @@ def pack_option_gen( cls=cmd_util.ApioOption, ) -serial_port_option = click.option( - "serial_port", # Var name. - "--serial-port", - type=str, - metavar="serial-port", - help="Set the serial port.", - cls=cmd_util.ApioOption, -) - verbose_option = click.option( "verbose", # Var name. diff --git a/apio/commands/report.py b/apio/commands/report.py index 3df1a0de..5de54eaf 100644 --- a/apio/commands/report.py +++ b/apio/commands/report.py @@ -46,23 +46,11 @@ @click.pass_context @options.project_dir_option @options.verbose_option -@options.top_module_option_gen(deprecated=True) -@options.board_option_gen(deprecated=True) -@options.fpga_option_gen(deprecated=True) -@options.size_option_gen(deprecated=True) -@options.type_option_gen(deprecated=True) -@options.pack_option_gen(deprecated=True) def cli( _: click.core.Context, # Options project_dir: Path, verbose: bool, - top_module: str, - board: str, - fpga: str, - size: str, - type_: str, - pack: str, ): """Analyze the design and report timing.""" @@ -75,13 +63,7 @@ def cli( # Run scons exit_code = scons.report( { - "board": board, - "fpga": fpga, - "size": size, - "type": type_, - "pack": pack, "verbose_pnr": verbose, - "top-module": top_module, } ) diff --git a/apio/commands/system.py b/apio/commands/system.py index 8506d904..b00036ac 100644 --- a/apio/commands/system.py +++ b/apio/commands/system.py @@ -82,7 +82,7 @@ cannot be mixed in the same command. [Advanced] The system configuration can be overriden using the system env -variable APIO_HOME_DIR, APIO_PACKAGES_DIR, APIO_PLATFORM. +variable APIO_HOME_DIR, and APIO_PLATFORM. """ diff --git a/apio/commands/time.py b/apio/commands/time.py deleted file mode 100644 index aaa6ab3a..00000000 --- a/apio/commands/time.py +++ /dev/null @@ -1,93 +0,0 @@ -# -*- coding: utf-8 -*- -# -- This file is part of the Apio project -# -- (C) 2016-2024 FPGAwars -# -- Authors -# -- * Jesús Arroyo (2016-2019) -# -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 -"""Implementation of 'apio' time' command""" - -import sys -from pathlib import Path -import click -from apio.managers.scons import SCons -from apio import cmd_util -from apio.commands import options -from apio.apio_context import ApioContext - - -# --------------------------- -# -- COMMAND -# --------------------------- -HELP = """ -The command 'apio time' has been deprecated. Please use the command -'apio report' instead. -""" - - -# pylint: disable=duplicate-code -# pylint: disable=too-many-arguments -# pylint: disable=too-many-positional-arguments -@click.command( - "time", - short_help="[Depreciated] Report design timing.", - help=HELP, - cls=cmd_util.ApioCommand, -) -@click.pass_context -@options.project_dir_option -@options.verbose_option -@options.verbose_yosys_option -@options.verbose_pnr_option -@options.top_module_option_gen(deprecated=False) -@options.board_option_gen(deprecated=False) -@options.fpga_option_gen(deprecated=False) -@options.size_option_gen(deprecated=False) -@options.type_option_gen(deprecated=False) -@options.pack_option_gen(deprecated=False) -def cli( - _: click.core.Context, - # Options - project_dir: Path, - verbose: bool, - verbose_yosys: bool, - verbose_pnr: bool, - # Deprecated options - top_module: str, - board: str, - fpga: str, - size: str, - type_: str, - pack: str, -): - """Analyze the design and report timing.""" - - click.secho( - "The 'apio time' command is deprecated. " - "Please use the 'apio report' command instead.", - fg="yellow", - ) - - # -- Create the apio context. - apio_ctx = ApioContext(project_dir=project_dir, load_project=True) - - # -- Create the scons manager. - scons = SCons(apio_ctx) - - # Run scons - exit_code = scons.time( - { - "board": board, - "fpga": fpga, - "size": size, - "type": type_, - "pack": pack, - "top-module": top_module, - "verbose_all": verbose, - "verbose_yosys": verbose_yosys, - "verbose_pnr": verbose_pnr, - } - ) - - # -- Done! - sys.exit(exit_code) diff --git a/apio/commands/uninstall.py b/apio/commands/uninstall.py deleted file mode 100644 index 2779663b..00000000 --- a/apio/commands/uninstall.py +++ /dev/null @@ -1,117 +0,0 @@ -# -*- coding: utf-8 -*- -# -- This file is part of the Apio project -# -- (C) 2016-2024 FPGAwars -# -- Authors -# -- * Jesús Arroyo (2016-2019) -# -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 -"""Implementation of 'apio uninstall' command""" - -import sys -from pathlib import Path -from typing import Tuple -from varname import nameof -import click -from apio.managers.old_installer import Installer -from apio import cmd_util -from apio.apio_context import ApioContext -from apio.commands import options, install - - -# R0801: Similar lines in 2 files -# pylint: disable=R0801 -def _uninstall(apio_ctx: ApioContext, packages: list, sayyes, verbose: bool): - """Uninstall the given list of packages""" - - # -- Ask the user for confirmation - if sayyes or click.confirm("Do you want to uninstall?"): - - # -- Uninstall packages, one by one - for package in packages: - - # -- The uninstalation is performed by the Installer object - modifiers = Installer.Modifiers( - force=False, checkversion=False, verbose=verbose - ) - installer = Installer(package, apio_ctx, modifiers) - - # -- Uninstall the package! - installer.uninstall() - - # -- User quit! - else: - click.secho("Abort!", fg="red") - - -# --------------------------- -# -- COMMAND -# --------------------------- -HELP = """ -The command 'apio uninstall' has been deprecated. Please use the -command 'apio packages' instead. -""" - - -# pylint: disable=duplicate-code -# pylint: disable=too-many-arguments -# pylint: disable=too-many-positional-arguments -@click.command( - "uninstall", - short_help="[Depreciated] Uninstall apio packages.", - help=HELP, - cls=cmd_util.ApioCommand, -) -@click.pass_context -@click.argument("packages", nargs=-1, required=False) -@options.list_option_gen(help="List all installed packages.") -@options.all_option_gen(help="Uninstall all packages.") -@options.project_dir_option -@options.sayyes -@options.verbose_option -def cli( - cmd_ctx: click.core.Context, - # Arguments - packages: Tuple[str], - # Options - list_: bool, - all_: bool, - project_dir: Path, - sayyes: bool, - verbose: bool, -): - """Implements the uninstall command.""" - - click.secho( - "The 'apio uninstall' command is deprecated. " - "Please use the 'apio packages' command instead.", - fg="yellow", - ) - - # Make sure these params are exclusive. - cmd_util.check_at_most_one_param(cmd_ctx, nameof(packages, list_, all_)) - - # -- Create the apio context. - apio_ctx = ApioContext(project_dir=project_dir, load_project=False) - - # -- Uninstall the given apio packages - if packages: - _uninstall(apio_ctx, packages, sayyes, verbose) - sys.exit(0) - - # -- Uninstall all the packages - if all_: - # -- Get all the installed apio packages - packages = apio_ctx.profile.packages - # -- Uninstall them! - _uninstall(apio_ctx, packages, sayyes, verbose) - sys.exit(0) - - # -- List all the packages (installed or not) - if list_: - # pylint: disable=protected-access - install._list_packages(apio_ctx) - # pylint: enable=protected-access - sys.exit(0) - - # -- Invalid option. Just show the help - click.secho(cmd_ctx.get_help()) diff --git a/apio/commands/upload.py b/apio/commands/upload.py index 1304de11..cb91e5f3 100644 --- a/apio/commands/upload.py +++ b/apio/commands/upload.py @@ -20,6 +20,23 @@ # --------------------------- # -- COMMAND SPECIFIC OPTIONS # --------------------------- +serial_port_option = click.option( + "serial_port", # Var name. + "--serial-port", + type=str, + metavar="serial-port", + help="Set the serial port.", + cls=cmd_util.ApioOption, +) + +ftdi_id_option = click.option( + "ftdi_id", # Var name. + "--ftdi-id", + type=str, + metavar="ftdi-id", + help="Set the FTDI id.", +) + sram_option = click.option( "sram", # Var name. "-s", @@ -65,20 +82,17 @@ cls=cmd_util.ApioCommand, ) @click.pass_context -@options.project_dir_option -@options.serial_port_option -@options.ftdi_id +@serial_port_option +@ftdi_id_option @sram_option @flash_option @options.verbose_option @options.verbose_yosys_option @options.verbose_pnr_option -@options.top_module_option_gen(deprecated=True) -@options.board_option_gen(deprecated=True) +@options.project_dir_option def cli( _: click.core.Context, # Options - project_dir: Path, serial_port: str, ftdi_id: str, sram: bool, @@ -86,9 +100,7 @@ def cli( verbose: bool, verbose_yosys: bool, verbose_pnr: bool, - # Deprecated options - top_module: str, - board: str, + project_dir: Path, ): """Implements the upload command.""" @@ -108,8 +120,6 @@ def cli( # -- Construct the configuration params to pass to SCons # -- from the arguments config = { - "board": board, - "top-module": top_module, "verbose_all": verbose, "verbose_yosys": verbose_yosys, "verbose_pnr": verbose_pnr, diff --git a/apio/commands/verify.py b/apio/commands/verify.py deleted file mode 100644 index f93cf910..00000000 --- a/apio/commands/verify.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: utf-8 -*- -# -- This file is part of the Apio project -# -- (C) 2016-2024 FPGAwars -# -- Authors -# -- * Jesús Arroyo (2016-2019) -# -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 -"""Implementation of 'apio verify' command""" - -import sys -from pathlib import Path -import click -from apio.managers.scons import SCons -from apio import cmd_util -from apio.commands import options -from apio.apio_context import ApioContext - - -# --------------------------- -# -- COMMAND -# --------------------------- - -HELP = """ -The command 'apio verify' has been deprecated. Please use the command -'apio lint' instead. -""" - - -@click.command( - "verify", - short_help="[Depreciated] Verify the verilog code.", - help=HELP, - cls=cmd_util.ApioCommand, -) -@click.pass_context -@options.project_dir_option -@options.verbose_option -@options.board_option_gen(deprecated=True) -def cli( - _: click.core.Context, - # Options - project_dir: Path, - verbose: bool, - # Deprecated options - board: str, -): - """Implements the verify command.""" - - click.secho( - "The 'apio verify' command is deprecated. " - "Please use the 'apio lint' command instead.", - fg="yellow", - ) - - # -- Crete the apio context. - apio_ctx = ApioContext(project_dir=project_dir, load_project=True) - - # -- Create the scons manager. - scons = SCons(apio_ctx) - - # -- Verify the project with the given parameters - exit_code = scons.verify( - { - "board": board, - "verbose_all": verbose, - } - ) - - # -- Done! - sys.exit(exit_code) diff --git a/apio/env_options.py b/apio/env_options.py index 91292d6a..8d94ae22 100644 --- a/apio/env_options.py +++ b/apio/env_options.py @@ -19,10 +19,6 @@ # -- specified, the 'packages' directory with the individual packages. APIO_HOME_DIR = "APIO_HOME_DIR" -# -- Env variable to override the apio packages dir ~/.apio/packages. -# -- If specified, it contains the installed packages directories such as -# -- 'examples' or 'tools-oss-cad-suite. -APIO_PACKAGES_DIR = "APIO_PACKAGES_DIR" # -- Env variable to override the platform id that is determined automatically # -- from the system properties. If specified, the value should match one @@ -32,7 +28,6 @@ # -- List of all supported env options. _SUPPORTED_APIO_VARS = [ APIO_HOME_DIR, - APIO_PACKAGES_DIR, APIO_PLATFORM, ] diff --git a/apio/managers/old_installer.py b/apio/managers/old_installer.py deleted file mode 100644 index 69c25018..00000000 --- a/apio/managers/old_installer.py +++ /dev/null @@ -1,512 +0,0 @@ -# -*- coding: utf-8 -*- -# -- This file is part of the Apio project -# -- (C) 2016-2021 FPGAwars -# -- Author Jesús Arroyo -# -- Licence GPLv2 -"""Implementation for the apio PACKAGES command""" - -import sys -import shutil - -from pathlib import Path -from dataclasses import dataclass -import click -import requests - -from apio import util -from apio.apio_context import ApioContext -from apio.managers.downloader import FileDownloader -from apio.managers.unpacker import FileUnpacker - - -# R0902: Too many instance attributes (12/7) (too-many-instance-attributes) -# pylint: disable=R0902 -# R0801: Similar lines in 2 files -# pylint: disable=R0801 -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 - verbose: bool - - def __init__( - self, - package: str, - apio_ctx: ApioContext = None, - modifiers=Modifiers(force=False, checkversion=True, verbose=False), - ): - """Class initialization. Parameters: - * package: Package name to manage/install. It can have a sufix with - the version. Ex. "system@1.1.2" - """ - - # -- Refactoring: Join together all the attributes - # -- This class has too many attributes (it is too complex) - # -- It should be refactor - # -- Al the attributes are shown together so that it is - # -- easier to refactor them in the future - self.package = None - self.version = None - self.force_install = None - self.packages_dir = None - self.apio_ctx = apio_ctx - self.spec_version = None - self.package_folder_name = None - self.extension = None - self.download_url = None - self.compressed_name = None - - # Parse version. The following attributes are used: - # * Installer.package: Package name (without version) - # * Installer.version: Package version (or None) - if "@" in package: - split = package.split("@") - self.package = split[0] - self.version = split[1] - else: - self.package = package - self.version = None - - # -- Attribute Installer.force_install - # -- Force installation or not - self.force_install = modifiers.force - - # -- Show detailed output. - self.verbose = modifiers.verbose - - # -- Installer.package_dir: path were the packages are stored - # -- Ex. /home/obijuan/.apio/packages - self.packages_dir = "" - - # -- Folder name were the packages are stored - # -- - # -- NOTE: we shouldn't assume the directory name since it can be - # -- overriden with APIO_PACKAGES_DIR but since this old installer is - # -- going away, we leave this as is. (Nov 2024) - dirname = "packages" - - # -- If the package is known... - # --(It is defined in the resources/packages.json file) - if self.package in self.apio_ctx.platform_packages: - # -- Store the package dir - self.packages_dir = apio_ctx.packages_dir / dirname - - # Get the metadata of the given package - package_info = self.apio_ctx.platform_packages[self.package] - - # Get the information about the valid versions - distribution = self.apio_ctx.distribution - - # Get the spectec package version - self.spec_version = distribution["packages"][self.package] - - # Get the package folder name under the packages root dir. - self.package_folder_name = package_info["release"]["folder_name"] - - # Get the extension given to the toolchain. Tipically tar.gz - self.extension = package_info["release"]["extension"] - - # Check if the version is ok (It is only done if the - # checkversion flag has been activated) - if modifiers.checkversion: - # Check version. The filename is read from the repostiroy. - # -- Get the url of the version file - url_version = package_info["release"]["url_version"] - - # -- Get the latest version - # -- It will exit in case of error - remote_version = self._get_latest_version(url_version) - - # -- It is only execute in case the version is valid - # -- it will exit otherwise - - # Store the valid version - self.version = remote_version - - # Get the plaform_os name - # e.g., [linux_x86_64, linux] - # platform_os = platform.split("_")[0] - - # Build the URLs for downloading the package - self.download_url = self._get_download_url(package_info) - - # -- The package is kwnown but the version is not correct - else: - if ( - self.package in self.apio_ctx.profile.packages - and modifiers.checkversion is False - ): - self.packages_dir = apio_ctx.home_dir / dirname - - self.package_folder_name = "toolchain-" + package - - # -- If the Installer.package_dir property was not assigned, - # -- is because the package was not known. Abort! - if not self.packages_dir: - click.secho(f"Error: no such package '{self.package}'", fg="red") - sys.exit(1) - - def _get_download_url(self, package_info: dict) -> str: - """Get the download URL for the given package - * INPUTS: - - package: Object with the package information: - * Respository - - name - - organization - * Release - - tag_name - - compressed_name - - uncompressed_name - - package_name - - extension - - url_version - * Description - - plaform: Destination platform (Ex. linux_x86_64) - * OUTPUT: The download URL - (Ex: 'https://github.com/FPGAwars/apio-examples/releases/ - download/0.0.35/apio-examples-0.0.35.zip') - """ - - # -- Get the compressed name - # -- It is in fact a template, with paramters: - # -- %V : Version - # -- %P : Platfom - # -- Ex: 'apio-examples-%V' - compressed_name = package_info["release"]["compressed_name"] - - # -- Replace the '%V' parameter with the package version - compressed_name_version = compressed_name.replace("%V", self.version) - - # -- Map Replace the '%P' parameter with the package selector of this - # -- platform (the package selectors are specified in platforms.json). - package_selector = self.apio_ctx.platforms[self.apio_ctx.platform_id][ - "package_selector" - ] - self.compressed_name = compressed_name_version.replace( - "%P", package_selector - ) - - # -- Get the uncompressed name. It is also a template with the - # -- same parameters: %V and %P - uncompressed_name = package_info["release"]["uncompressed_name"] - - # -- Replace the '%V' parameter - uncompress_name_version = uncompressed_name.replace("%V", self.version) - - # -- Replace the '%P' parameter - self.uncompressed_name = uncompress_name_version.replace( - "%P", self.apio_ctx.platform_id - ) - - # -- Build the package tarball filename - # --- Ex. 'apio-examples-0.0.35.zip' - tarball = f"{self.compressed_name}.{self.extension}" - - # -- Build the Download URL! - name = package_info["repository"]["name"] - organization = package_info["repository"]["organization"] - tag = package_info["release"]["tag_name"].replace("%V", self.version) - - download_url = ( - f"https://github.com/{organization}/{name}/releases/" - f"download/{tag}/{tarball}" - ) - - return download_url - - # W0703: Catching too general exception Exception (broad-except) - # pylint: disable=W0703 - def install(self): - """Install the current package set in the Installer Object""" - - click.secho(f"Installing {self.package} package:", fg="cyan") - - # -- Create the apio package folder, if it does not exit - self.packages_dir.mkdir(exist_ok=True) - - # -- The first step is downloading the package - # -- This variable stores the path to the packages - dlpath = None - - try: - # -- Try downloading the file - dlpath = self._download(self.download_url) - - # -- There is no write access to the package folder - except IOError as exc: - click.secho( - "Warning: permission denied in packages directory", fg="yellow" - ) - click.secho(str(exc), fg="red") - - # -- In case of any other error, try to install with the other - # -- url... - # --- ummm very likely this second installation can be removed... - # except Exception: - # Try os name - # dlpath = self._install_os_package(platform_download_url) - except util.ApioException: - click.secho("Error: package not found\n", fg="red") - - # -- Second step: Install downloaded package - self._install_package(dlpath) - - # -- Rename unpacked dir to package dir - self._rename_unpacked_dir() - - def _install_package(self, dlpath: Path): - """Install the given tarball""" - - # -- Make sure there is a non-null filepath - if dlpath: - - # -- Build the destination path - # -- Ex. '/home/obijuan/.apio/packages/examples' - package_dir = self.packages_dir / self.package_folder_name - - if self.verbose: - click.secho(f"Package dir: {package_dir.absolute()}") - - # -- Destination path is a folder (Already exist!) - if package_dir.is_dir(): - - # -- Remove it! - shutil.rmtree(package_dir) - - # -- The packages that have the property uncompressed_name - # -- have a folder (with that name) inside the compresssed file - # -- Ex. The package examples has the folder apio-examples-0.0.35 - # -- Because of this, it should be unpacked directly in the - # -- packages folder (Ex. /home/obijuan/.paio/packages) and then - # -- rename the folder to the package name - # -- (Ex. apio-examples-0.0.35 -> examples) - if self.uncompressed_name: - - # -- Uncompress it!! - # -- Ex. folder: /home/obijuan/.apio/packages - self._unpack(dlpath, self.packages_dir) - - # -- In this other case the package is directly - # -- unpack in the package_dir folder - # -- Ex. packages/tools-oss-cad-suite - else: - self._unpack(dlpath, package_dir) - - # -- Remove the downloaded compress file - # -- Ex. remove '/home/obijuan/.apio/packages/ - # apio-examples-0.0.35.zip' - dlpath.unlink() - - # -- Add package to profile - self.apio_ctx.profile.add_package(self.package, self.version) - - # -- Save the profile - self.apio_ctx.profile.save() - - # -- Inform the user! - click.secho( - f"""Package \'{self.package}\' has been """ - """successfully installed!""", - fg="green", - ) - - def _rename_unpacked_dir(self): - """Change the name of the downloaded file to the final one - Ex. '/home/obijuan/.apio/packages/apio-examples-0.0.35' - ---> '/home/obijuan/.apio/packages/examples' - Only for packages that has the property uncompressed_name - """ - - if self.uncompressed_name: - - # -- Build the names - # -- src folder (the one downloaded and installed) - # -- Ex. '/home/obijuan/.apio/packages/apio-examples-0.0.35' - unpack_dir = self.packages_dir / self.uncompressed_name - - # -- New folder - # -. Ex, '/home/obijuan/.apio/packages/examples' - package_dir = self.packages_dir / self.package_folder_name - - # -- Rename it! - if unpack_dir.is_dir(): - unpack_dir.rename(package_dir) - - def uninstall(self): - """Uninstall the apio package""" - - # -- Build the package filename - file = self.packages_dir / self.package_folder_name - - if self.verbose: - click.secho(f"Package dir: {file.absolute()}") - - # -- Check that it is a folder... - if file.is_dir(): - - # -- Inform the user - package_color = click.style(self.package, fg="cyan") - click.secho(f"Uninstalling {package_color} package:") - - # -- Remove the folder with all its content!! - shutil.rmtree(file) - - # -- Inform the user - click.secho( - f"Package '{self.package}' has been " - "successfully uninstalled.", - fg="green", - ) - else: - # -- Package not installed. We treat it as a success. - click.secho( - f"Package '{self.package}' was not installed.", fg="green" - ) - - # -- Remove the package from the profile file - self.apio_ctx.profile.remove_package(self.package) - self.apio_ctx.profile.save() - - @staticmethod - def _get_tarball_name(name, extension): - tarball = f"{name}.{extension}" - return tarball - - def _get_latest_version(self, url_version: str) -> str: - """Get the latest recommanded version from the given remote - version file. The file is downloaded and the version is - read and returned - - - INPUTS: - * url_version: URL of the package's version file - Ex. https://github.com/FPGAwars/apio-examples/raw/master/ - VERSION - - The url_version for every package is located in the file: - resources/packages.json - - - OUTPUT: A string with the package version (Ex. '0.0.35') - """ - - if self.version: - # -- No checking... return the required version - return self.version - - # -- Find latest version number released. It is found using the - # -- version url package configuration. - if url_version: - if self.verbose: - click.secho(f"Version url: {url_version}") - - # -- Get the version file with the latest version number - req = requests.get(url_version, timeout=5) - - # -- Check the server response - if ( - # pylint: disable=no-member - req.status_code - == requests.codes.ok - ): - # -- Request OK - print("Remote version file downloaded.") - - # -- Read the version without the ending \n - version = req.text.rstrip("\n") - - # -- Debug - print(f"Remote version: {version}") - - # -- Return the version - return version - - # -- There was a problem with the request - click.secho("Error downloading the version file", fg="red") - click.secho(f"URL: {url_version}", fg="red") - click.secho(f"Error code: {req.status_code}", fg="red") - sys.exit(1) - - # -- Error: No URL defined for the version file - click.secho( - "No URL defined for the version file\n" - + "It is not possible to get the latest version number", - fg="red", - ) - click.secho("Check the resources/packages.json file", fg="red") - sys.exit(1) - - def _download(self, url: str) -> str: - """Download the given file (url). Return the path of - the destination file - * INPUTS: - * url: File to download - * OUTPUTS: - * The path of the destination file - """ - - # -- Check the installed version of the package - installed_ok = self.apio_ctx.profile.is_installed_version_ok( - self.package, self.version, self.verbose - ) - - # -- Package already installed, and no force_install flag - # -- Nothing to download - if installed_ok and not self.force_install: - click.secho( - f"Already installed. Version {self.version}", - fg="yellow", - ) - return None - - # ----- Download the package! - if self.verbose: - click.secho(f"Src url: {url}") - - # -- Object for downloading the file - filed = FileDownloader(url, self.packages_dir) - - # -- Get the destination path - filepath = filed.destination - - # -- Inform the user - if self.verbose: - click.secho(f"Local file: {filepath}") - - # -- Download start! - try: - filed.start() - - # -- If the user press Ctrl-C (Abort) - except KeyboardInterrupt: - - # -- Remove the file - if filepath.is_file(): - filepath.unlink() - - # -- Inform the user - click.secho("Abort download!", fg="red") - sys.exit(1) - - # -- Return the destination path - return filepath - - @staticmethod - def _unpack(pkgpath: Path, pkgdir: Path): - """Unpack the given file, in the pkgdir - * INPUTS: - - pkgpath: File to unpack - - pkgdir: Destination path - """ - - # -- Build the unpacker object - fileu = FileUnpacker(pkgpath, pkgdir) - - # -- Unpack it! - success = fileu.start() - - return success diff --git a/apio/managers/project.py b/apio/managers/project.py index 44e0a43b..989921f7 100644 --- a/apio/managers/project.py +++ b/apio/managers/project.py @@ -99,6 +99,15 @@ def _validate(self, resolver: ProjectResolver): self._options["board"] ) + # -- If top-module was not specified, fill in the default value. + if "top-module" not in self._options: + self._options["top-module"] = DEFAULT_TOP_MODULE + click.secho( + "Project file has no 'top-module', " + f"assuming '{DEFAULT_TOP_MODULE}'.", + fg="yellow", + ) + def __getitem__(self, option: str) -> Optional[str]: # -- If this fails, this is a programming error. assert option in ALL_OPTIONS, f"Invalid project option: [{option}]" @@ -120,8 +129,8 @@ def load_project_from_file( # -- Currently, apio.ini is still optional so we just warn. if not file_path.exists(): - click.secho(f"Info: Project has no {APIO_INI} file.", fg="yellow") - return None + click.secho(f"Error: missing project file {APIO_INI}.", fg="red") + sys.exit(1) # -- Read and parse the file. parser = ConfigParser() @@ -194,45 +203,3 @@ def create_project_file( fg="green", ) return True - - -def modify_project_file( - project_dir: Path, - board: Optional[str], - top_module: Optional[str], -) -> bool: - """Update the current ini file with the given optional parameters. - Returns True if ok. Board is assumed to be None or a canonical id of an - exiting board (caller should validate)""" - - # -- construct the file path. - ini_path = project_dir / APIO_INI - - # -- Check if the apio.ini file exists - if not ini_path.is_file(): - click.secho( - f"Error: '{ini_path}' not found. You should create it first.\n" - "see 'apio create -h' for more details.", - fg="red", - ) - return False - - # -- Read the current apio.ini file - config = ConfigObj(str(ini_path)) - - # -- Set specified fields. - if board: - config["env"]["board"] = board - - if top_module: - config["env"]["top-module"] = top_module - - # -- Write the apio ini file - config.write() - - click.secho( - f"File '{ini_path}' was modified successfully.\n" - f"Run the apio clean command for project consistency.", - fg="green", - ) - return True diff --git a/apio/managers/scons.py b/apio/managers/scons.py index 8b1e91e1..261141b1 100644 --- a/apio/managers/scons.py +++ b/apio/managers/scons.py @@ -94,23 +94,6 @@ def clean(self, args) -> int: "-c", arch=arch, variables=variables, required_packages_names=[] ) - @on_exception(exit_code=1) - def verify(self, args) -> int: - """Runs a scons subprocess with the 'verify' target. Returns process - exit code, 0 if ok.""" - - # -- Split the arguments - variables, __, arch = process_arguments(self.apio_ctx, args) - - # -- Execute scons!!! - # -- The packages to check are passed - return self._run( - "verify", - variables=variables, - arch=arch, - required_packages_names=["oss-cad-suite"], - ) - @on_exception(exit_code=1) def graph(self, args) -> int: """Runs a scons subprocess with the 'graph' target. Returns process @@ -189,29 +172,6 @@ def build(self, args) -> int: required_packages_names=["oss-cad-suite"], ) - @on_exception(exit_code=1) - def time(self, args) -> int: - """Runs a scons subprocess with the 'time' target. Returns process - exit code, 0 if ok.""" - - variables, board, arch = process_arguments(self.apio_ctx, args) - - if arch not in ["ice40"]: - click.secho( - "Error: Time analysis for " - f"{arch.upper()} is not supported yet.", - fg="red", - ) - return 99 - - return self._run( - "time", - variables=variables, - board=board, - arch=arch, - required_packages_names=["oss-cad-suite"], - ) - @on_exception(exit_code=1) def report(self, args) -> int: """Runs a scons subprocess with the 'report' target. Returns process diff --git a/apio/managers/scons_args.py b/apio/managers/scons_args.py index b4ccdf66..09857fd3 100644 --- a/apio/managers/scons_args.py +++ b/apio/managers/scons_args.py @@ -10,14 +10,12 @@ from functools import wraps from typing import Dict, Tuple, Optional, List, Any import click -from apio.managers.project import DEFAULT_TOP_MODULE from apio.apio_context import ApioContext # -- Names of supported args. Unless specified otherwise, all args are optional # -- and have a string value. Values such as None, "", or False, which # -- evaluate to a boolean False are considered 'no value' and are ignored. -ARG_BOARD_ID = "board" ARG_FPGA_ID = "fpga" ARG_FPGA_ARCH = "arch" ARG_FPGA_TYPE = "type" @@ -100,76 +98,51 @@ def outer(*args): class Arg: """Represent an arg.""" - def __init__(self, arg_name: str, var_name: str = None): - """If var_name exists, the arg is mapped to scons varialbe with - that name.""" - self._name: str = arg_name - self._var_name: Optional[str] = var_name - self._has_value: bool = False + def __init__(self, arg_name: str, var_name: str): + """The arg is mapped from arg_name to to scons varialbe var_name.""" + assert isinstance(arg_name, str) + assert isinstance(var_name, str) + self.arg_name: str = arg_name + self.var_name: Optional[str] = var_name + self.has_value: bool = False self._value: Any = None def __repr__(self): """Object representation, for debugging.""" - result = f"Arg '{self._name}'" - if self._has_value: + result = f"Arg '{self.arg_name}'" + if self.has_value: result += f" = {self._value.__repr__()}" else: result += " (no value)" return result - @property - def name(self): - """Returns the arg name.""" - return self._name - - @property - def has_value(self): - """True if a value was set.""" - return self._has_value - def set(self, value: Any): """Sets a value. Value cannot be None, "", False, or any other value that is evaluated to a bool False. Value can be set only once, unless if writing the same value.""" # -- These are programming errors, not user error. - assert value, f"Setting arg '{self._name}' with a None value." - if not self._has_value: + assert value, f"Setting arg '{self.arg_name}' with a None value." + if not self.has_value: self._value = value - self._has_value = True + self.has_value = True elif value != self._value: raise ValueError( f"contradictory argument values: " - f"'{self._name}' = ({value} vs {self._value})" + f"'{self.arg_name}' = ({value} vs {self._value})" ) @property def value(self): """Returns the value. hould be called only if has_value is True.""" - assert self._has_value, f"Arg '{self._name}' has no value to read." + assert self.has_value, f"Arg '{self.arg_name}' has no value to read." return self._value def value_or(self, default): """Returns value or default if no value.""" - if self._has_value: + if self.has_value: return self._value return default - @property - def has_var_value(self): - """Does the arg contain a value that should be exported as a scons - variable? To qualify, it needs to has a var name and a value. - To qualify, it needs to""" - return self._var_name and self._has_value - - @property - def var_name(self): - """Returns the name of the scons variable. Should be called only - if the arg is known to have a variable name.""" - assert ( - self._var_name is not None - ), f"Arg '{self._name}' has no var arg." - return self._var_name - # R0912: Too many branches (14/12) # pylint: disable=R0912 @@ -183,8 +156,8 @@ def process_arguments( provided scons args. The list of the valid entires in the args ditct, see ARG_XX definitions above. - * apio_ctx: ApioContext of this apio invocation. Should be created with - 'load_project' = True. + * apio_ctx: ApioContext of this apio invocation. Should have the + project file loaded. * args: a Dictionary with the scons args. * OUTPUT: * Return a tuple (variables, board, arch) @@ -195,16 +168,9 @@ def process_arguments( - arch: FPGA architecture ('ice40', 'ecp5'...) """ - # -- We expect here only contexts at project scope. Currently apio.ini - # -- is still not required so apio_ctx.project can still be None. - assert ( - apio_ctx.project_loading_requested - ), "Apio context not at project scope." - # -- Construct the args dictionary with all supported args. Most of the # -- args also have the name of their exported scons variable. args: Dict[str, Arg] = { - ARG_BOARD_ID: Arg(ARG_BOARD_ID), ARG_FPGA_ID: Arg(ARG_FPGA_ID, "fpga_model"), ARG_FPGA_ARCH: Arg(ARG_FPGA_ARCH, "fpga_arch"), ARG_FPGA_TYPE: Arg(ARG_FPGA_TYPE, "fpga_type"), @@ -232,57 +198,23 @@ def process_arguments( if seed_value: args[arg_name].set(seed_value) - # -- Keep a shortcut, for convinience. Note that project can be None - # -- if the project doesn't have a apio.ini file. + # -- Get the project object. All commands that invoke scons are expected + # -- to be in a project context. + assert apio_ctx.has_project_loaded, "Scons encountered a missing project." project = apio_ctx.project - # -- Board name given in the command line - if args[ARG_BOARD_ID].has_value: - - # -- If there is a project file (apio.ini) the board - # -- given by command line overrides it - # -- (command line has the highest priority) - if project and 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 args[ARG_BOARD_ID].value != project["board"]: - click.secho( - "Info: ignoring board specification from apio.ini.", - fg="yellow", - ) - - # -- Try getting the board id from the project - else: - # -- ...read it from the apio.ini file - if project and project["board"]: - args[ARG_BOARD_ID].set(project["board"]) + # -- Get project's board. It should be prevalidated when loading the + # -- project, but we sanity check it again just in case. - # update_arg(args, ARG_BOARD, project.board) + board = project["board"] + assert board is not None, "Scons got a None board." + assert board in apio_ctx.boards, f"Unknown board id [{board}]" - # -- The board is given (either by arguments or by project file) - if args[ARG_BOARD_ID].has_value: - - # -- Check that the board id is valid. - if args[ARG_BOARD_ID].value not in apio_ctx.boards: - raise ValueError(f"unknown board: {args[ARG_BOARD_ID].value}") - - # -- Read the FPGA name for the current board - fpga = apio_ctx.boards.get(args[ARG_BOARD_ID].value).get(ARG_FPGA_ID) - - # -- Add it to the current configuration - if fpga: - args[ARG_FPGA_ID].set(fpga) - - # -- Check that the FPGA was given - if not args[ARG_FPGA_ID].has_value: - perror_insuficient_arguments() - raise ValueError("Missing FPGA") - - # -- Check that the FPGA is valid - if args[ARG_FPGA_ID].value not in apio_ctx.fpgas: - raise ValueError(f"unknown FPGA: {args[ARG_FPGA_ID].value}") + # -- Read the FPGA name for the current board + fpga = apio_ctx.boards.get(board).get("fpga") + assert fpga, "process_arguments(): fpga assertion failed." + assert fpga in apio_ctx.fpgas, f"process_arguments(): unknown fpga {fpga} " + args[ARG_FPGA_ID].set(fpga) # -- Update the FPGA items according to the current board and fpga # -- Raise an exception in case of a contradiction @@ -312,25 +244,12 @@ def process_arguments( # -- Config item not defined!! it is mandatory! if not arg.has_value: perror_insuficient_arguments() - raise ValueError(f"Missing FPGA {arg.name.upper()}") + raise ValueError(f"Missing FPGA {arg.arg_name.upper()}") - # -- If top-module not specified by the user, determine what value to use. + # -- If top-module not specified by the user (e.g. for apio graph command), + # -- use the top module from the project file. if not args[ARG_TOP_MODULE].has_value: - - if project and project["top-module"]: - # -- If apio.ini has a top-module value use it. - - args[ARG_TOP_MODULE].set(project["top-module"]) - else: - - # -- Use the default top-level - args[ARG_TOP_MODULE].set(DEFAULT_TOP_MODULE) - - click.secho( - "Warning: 'top-module' is not specified in apio.ini, " - f"assuming: '{DEFAULT_TOP_MODULE}'", - fg="yellow", - ) + args[ARG_TOP_MODULE].set(project["top-module"]) # -- Set the platform id. assert apio_ctx.platform_id, "Missing platform_id in apio context" @@ -342,13 +261,13 @@ def process_arguments( # -- SConstruct arg parsing. variables = [] for arg in args.values(): - if arg.has_var_value: + if arg.has_value: variables.append(f"{arg.var_name}={arg.value}") # -- All done. return ( variables, - args[ARG_BOARD_ID].value_or(None), + board, args[ARG_FPGA_ARCH].value_or(None), ) diff --git a/apio/pkg_util.py b/apio/pkg_util.py index db9674f9..60f004f4 100644 --- a/apio/pkg_util.py +++ b/apio/pkg_util.py @@ -191,9 +191,9 @@ def _check_required_package( # -- Case 1: Package is not installed. if current_version is None: click.secho( - f"Error: package '{package_name}' is not installed.", fg="red" + f"Error: apio package '{package_name}' is not installed.", fg="red" ) - _show_package_install_instructions(package_name) + click.secho("Please run:\n apio packages --install", fg="yellow") sys.exit(1) # -- Case 2: Version does not match requirmeents. @@ -204,8 +204,9 @@ def _check_required_package( f"match the requirement for version {spec_version}.", fg="red", ) - - _show_package_install_instructions(package_name) + click.secho( + "Please run:\n apio packages --install --force", fg="yellow" + ) sys.exit(1) # -- Case 3: The package's directory does not exist. @@ -213,7 +214,13 @@ def _check_required_package( if package_dir and not package_dir.is_dir(): message = f"Error: package '{package_name}' is installed but missing" click.secho(message, fg="red") - _show_package_install_instructions(package_name) + click.secho( + "Please run:\n" + " apio packages --fix\n" + " apio packages --install -- force", + fg="yellow", + ) + sys.exit(1) @@ -242,18 +249,6 @@ def _version_matches(current_version: str, spec_version: str) -> bool: return semver in spec -def _show_package_install_instructions(package_name: str): - """Prints hints on how to install a package with a given name.""" - - click.secho( - "Please run:\n" - f" apio packages --install --force {package_name}\n" - "or:\n" - " apio packages --install --force", - fg="yellow", - ) - - @dataclass class PackageScanResults: """Represents results of packages scan.""" diff --git a/apio/profile.py b/apio/profile.py index e72b152b..77a0483d 100644 --- a/apio/profile.py +++ b/apio/profile.py @@ -7,8 +7,6 @@ import json from pathlib import Path -import click -import semantic_version class Profile: @@ -32,41 +30,6 @@ def __init__(self, home_dir: Path): # -- Read the profile from file self.load() - def is_installed_version_ok(self, name: str, version: str, verbose: bool): - """Check the if the given package version is installed - * INPUT: - - name: Package name - - version: Version to install - * OUTPUT: - - True: Version installed, with the given version - - False: - - Package not installed - - Package installed but different version - """ - - # -- If the package is installed... - if name in self.packages: - - # -- Get the current version - pkg_version = self.get_package_installed_version(name) - - # -- Compare versions: current vs version to install - current_ver = semantic_version.Version(pkg_version) - to_install_ver = semantic_version.Version(version) - - if verbose: - click.secho(f"Current version: {current_ver}") - - same_versions = current_ver == to_install_ver - - # -- Return the state of the installed package: - # -- True: Package installed (with the given version) - # -- False: Package installed (but different version) - return same_versions - - # -- Package not installed - return False - def add_package(self, name: str, version: str): """Add a package to the profile class""" diff --git a/apio/resources/packages.json b/apio/resources/packages.json index 376ab488..b481abbc 100644 --- a/apio/resources/packages.json +++ b/apio/resources/packages.json @@ -10,7 +10,7 @@ "uncompressed_name": "apio-examples-%V", "folder_name": "examples", "extension": "zip", - "url_version": "https://github.com/FPGAwars/apio-examples/raw/master/VERSION" + "url_version": "https://github.com/FPGAwars/apio-examples/raw/master/VERSION_DEV" }, "description": "Apio's project examples", "env": {} diff --git a/apio/scons/ecp5/SConstruct b/apio/scons/ecp5/SConstruct index 13e0e548..73fd5752 100644 --- a/apio/scons/ecp5/SConstruct +++ b/apio/scons/ecp5/SConstruct @@ -117,7 +117,7 @@ synth_srcs, test_srcs = get_source_files(env) LPF = get_constraint_file(env, ".lpf", TOP_MODULE) -# -- Apio build/upload/time/report. +# -- Apio build/upload/report. # -- Builder (yosys, Synthesis). # -- (modules).v -> hardware.json. synth_builder = Builder( @@ -145,7 +145,7 @@ def pnr_emitter(target, source, env): return target, source -# -- Apio build/upload/time/report. +# -- Apio build/upload/report. # -- builder (nextpnr, Place and route). # -- hardware.json -> hardware.asc, hardware.pnr. pnr_builder = Builder( @@ -214,25 +214,6 @@ upload_target = env.Alias("upload", bin_target, programmer_cmd) env.AlwaysBuild(upload_target) -# -- Apio verify. -# -- Builder (iverilog, verilog compiler). -# -- (modules + testbenches).v -> hardware.out. -iverilog_verify_builder = Builder( - action=make_iverilog_action( - env, - ivl_path=IVL_PATH, - verbose=VERBOSE_ALL, - vcd_output_name="dummy_vcd_output", - is_interactive=False, - lib_dirs=[YOSYS_LIB_DIR], - ), - suffix=".out", - src_suffix=".v", - source_scanner=verilog_src_scanner, -) -env.Append(BUILDERS={"IVerilogVerify": iverilog_verify_builder}) - - # -- Apio sim/test # -- Builder helper (iverolog command generator). # -- (modules + testbench).v -> (testbench).out. @@ -302,14 +283,6 @@ vcd_builder = Builder( env.Append(BUILDERS={"VCD": vcd_builder}) -# -- Apio verify. -# -- Targets -# -- (modules).v -> (modules).out -verify_out_target = env.IVerilogVerify(TARGET, synth_srcs + test_srcs) -env.AlwaysBuild(verify_out_target) -verify_target = env.Alias("verify", verify_out_target) - - # -- Apio graph. # -- Targets. # -- (modules).v -> hardware.dot -> hardware.svg. diff --git a/apio/scons/gowin/SConstruct b/apio/scons/gowin/SConstruct index 3e9d08f4..610e98eb 100644 --- a/apio/scons/gowin/SConstruct +++ b/apio/scons/gowin/SConstruct @@ -113,7 +113,7 @@ synth_srcs, test_srcs = get_source_files(env) # -- Get the CST file name. CST = get_constraint_file(env, ".cst", TOP_MODULE) -# -- Apio build/upload/time/report. +# -- Apio build/upload/report. # -- Builder (yosys, Synthesis). # -- (modules).v -> hardware.json. synth_builder = Builder( @@ -141,7 +141,7 @@ def pnr_emitter(target, source, env): return target, source -# -- Apio build/upload/time/report. +# -- Apio build/upload/report. # -- builder (nextpnr, Place and route). # -- hardware.json -> hardware.pnr.json. pnr_builder = Builder( @@ -206,25 +206,6 @@ upload_target = env.Alias("upload", bin_target, programmer_cmd) env.AlwaysBuild(upload_target) -# -- Apio verify. -# -- Builder (iverilog, verilog compiler). -# -- (modules + testbenches).v -> hardware.out. -iverilog_verify_builder = Builder( - action=make_iverilog_action( - env, - ivl_path=IVL_PATH, - verbose=VERBOSE_ALL, - vcd_output_name="dummy_vcd_output", - is_interactive=False, - lib_dirs=[YOSYS_LIB_DIR], - ), - suffix=".out", - src_suffix=".v", - source_scanner=verilog_src_scanner, -) -env.Append(BUILDERS={"IVerilogVerify": iverilog_verify_builder}) - - # -- Apio sim/test # -- Builder helper (iverolog command generator). # -- (modules + testbench).v -> (testbench).out. @@ -295,14 +276,6 @@ vcd_builder = Builder( env.Append(BUILDERS={"VCD": vcd_builder}) -# -- Apio verify. -# -- Targets -# -- (modules).v -> (modules).out -verify_out_target = env.IVerilogVerify(TARGET, synth_srcs + test_srcs) -env.AlwaysBuild(verify_out_target) -verify_target = env.Alias("verify", verify_out_target) - - # -- Apio graph. # -- Targets. # -- (modules).v -> hardware.dot -> hardware.svg. diff --git a/apio/scons/ice40/SConstruct b/apio/scons/ice40/SConstruct index b972b178..ef738784 100644 --- a/apio/scons/ice40/SConstruct +++ b/apio/scons/ice40/SConstruct @@ -116,7 +116,7 @@ synth_srcs, test_srcs = get_source_files(env) PCF = get_constraint_file(env, ".pcf", TOP_MODULE) -# -- Apio build/upload/time/report. +# -- Apio build/upload/report. # -- Builder (yosys, Synthesis). # -- (modules).v -> hardware.json. synth_builder = Builder( @@ -144,7 +144,7 @@ def pnr_emitter(target, source, env): return target, source -# -- Apio build/upload/time/report. +# -- Apio build/upload/report. # -- builder (nextpnr, Place and route). # -- hardware.json -> hardware.asc, hardware.pnr. pnr_builder = Builder( @@ -175,20 +175,7 @@ bitstream_builder = Builder( env.Append(BUILDERS={"Bin": bitstream_builder}) -# -- Apio time. -# -- Builder | icetime | Time reports. -# -- hardware.asc -> hardware.rpt -time_rpt_builder = Builder( - action='icetime -d {0}{1} -P {2} -C "{3}" -mtr $TARGET $SOURCE'.format( - FPGA_TYPE, FPGA_SIZE, FPGA_PACK, CHIPDB_PATH - ), - suffix=".rpt", - src_suffix=".asc", -) -env.Append(BUILDERS={"Time": time_rpt_builder}) - - -# -- Apio build/upload/time/report. +# -- Apio build/upload/report. # -- Targets. # -- (module).v -> hardware.json -> hardware.asc -> hardware.bin. synth_target = env.Synth(TARGET, [synth_srcs]) @@ -222,34 +209,6 @@ upload_target = env.Alias("upload", bin_target, programmer_cmd) env.AlwaysBuild(upload_target) -# -- Apio time. -# -- Targets. -# -- hardware.asc -> hardware.rpt -time_rpt_target = env.Time(pnr_target) -env.AlwaysBuild(time_rpt_target) -time_target = env.Alias("time", time_rpt_target) - - -# -- Apio verify. -# -- Builder (iverilog, verilog compiler). -# -- (modules + testbenches).v -> hardware.out. -iverilog_verify_builder = Builder( - action=make_iverilog_action( - env, - ivl_path=IVL_PATH, - verbose=VERBOSE_ALL, - vcd_output_name="dummy_vcd_output", - is_interactive=False, - extra_params=["-DNO_ICE40_DEFAULT_ASSIGNMENTS"], - lib_files=[YOSYS_LIB_FILE], - ), - suffix=".out", - src_suffix=".v", - source_scanner=verilog_src_scanner, -) -env.Append(BUILDERS={"IVerilogVerify": iverilog_verify_builder}) - - # -- Apio sim/test # -- Builder helper (iverolog command generator). # -- (modules + testbench).v -> (testbench).out. @@ -320,14 +279,6 @@ vcd_builder = Builder( env.Append(BUILDERS={"VCD": vcd_builder}) -# -- Apio verify. -# -- Targets -# -- (modules).v -> (modules).out -verify_out_target = env.IVerilogVerify(TARGET, synth_srcs + test_srcs) -env.AlwaysBuild(verify_out_target) -verify_target = env.Alias("verify", verify_out_target) - - # -- Apio graph. # -- Targets. # -- (modules).v -> hardware.dot -> hardware.svg. diff --git a/apio/scons/scons_util.py b/apio/scons/scons_util.py index 17e9d05d..fcfa8a86 100644 --- a/apio/scons/scons_util.py +++ b/apio/scons/scons_util.py @@ -87,11 +87,16 @@ def basename(env: SConsEnvironment, file_name: str) -> str: return result -def is_verilog_src(env: SConsEnvironment, file_name: str) -> str: +def is_verilog_src( + env: SConsEnvironment, file_name: str, *, include_sv: bool = True +) -> bool: """Given a file name, determine by its extension if it's a verilog source - file (testbenches included).""" + file (testbenches included). If include_sv is True, include also + system verilog files.""" _, ext = os.path.splitext(file_name) - return ext == ".v" + if include_sv: + return ext in [".v", ".sv"] + return ext in [".v"] def has_testbench_name(env: SConsEnvironment, file_name: str) -> bool: @@ -376,21 +381,35 @@ def verilog_src_scanner_func( Returns a list of files. """ - # Sanity check. Should be called only to scan verilog files. - assert file_node.name.lower().endswith( - ".v" - ), f"Not a .v file: {file_node.name}" + # Sanity check. Should be called only to scan verilog files. If this + # fails, this is a programming error rather than a user error. + assert is_verilog_src( + env, file_node.name + ), f"Not a src file: {file_node.name}" + + # Create the initial set. All source file depend on apio.ini and must + # be built when apio.ini changes. + # + # pylint: disable=fixme + # TODO: add also other config files aush as boards.json and + # programmers.json. Since they are optional, need to figure out how to + # handle the case when they are deleted or created. includes_set = set() + includes_set.add("apio.ini") + # If the file doesn't exist, this returns an empty string. file_text = file_node.get_text_contents() # Get IceStudio includes. includes = icestudio_list_re.findall(file_text) includes_set.update(includes) + # Get Standard verilog includes. includes = verilog_include_re.findall(file_text) includes_set.update(includes) + # Get a deterministic list. (Does it sort by file.name?) includes_list = sorted(list(includes_set)) + # For debugging # info(env, f"*** {file_node.name} includes {includes_list}") return env.File(includes_list) @@ -492,8 +511,15 @@ def get_source_files(env: SConsEnvironment) -> Tuple[List[str], List[str]]: If a .v file has the suffix _tb.v it's is classified st a testbench, otherwise as a synthesis file. """ - # -- Get a list of all *.v files in the project dir. - files: List[File] = env.Glob("*.v") + # -- Get a list of all *.v and .sv files in the project dir. + files: List[File] = env.Glob("*.sv") + if files: + click.secho( + "Warning: project contains .sv files, system-verilog support " + "is experimental.", + fg="yellow", + ) + files = files + env.Glob("*.v") # Split file names to synth files and testbench file lists synth_srcs = [] @@ -640,8 +666,10 @@ def make_iverilog_action( escaped_vcd_output_name = vcd_output_name.replace("\\", "\\\\") # -- Construct the action string. + # -- The -g2012 is for system-verilog support and we use it here as an + # -- experimental feature. action = ( - "iverilog {0} {1} -o $TARGET {2} {3} {4} {5} {6} $SOURCES" + "iverilog -g2012 {0} {1} -o $TARGET {2} {3} {4} {5} {6} $SOURCES" ).format( ivl_path_param, "-v" if verbose else "", diff --git a/test-boards/Alhambra-II/test_leds.py b/test-boards/Alhambra-II/test_leds.py index 8a0bccb3..a9869624 100644 --- a/test-boards/Alhambra-II/test_leds.py +++ b/test-boards/Alhambra-II/test_leds.py @@ -15,8 +15,8 @@ # -- apio clean entry point from apio.commands.clean import cli as apio_clean -# -- apio verify entry point -from apio.commands.verify import cli as apio_verify +# -- apio lint entry point +from apio.commands.lint import cli as apio_lint # -- apio time entry point from apio.commands.upload import cli as apio_upload @@ -65,18 +65,18 @@ def test_ledon_build(): assert "icepack" in result.output -def test_ledon_verify(): - """Test the apio verify command""" +def test_ledon_lint(): + """Test the apio lint command""" # ---------------------------- - # -- Execute "apio verify" + # -- Execute "apio lint" # ---------------------------- - result = CliRunner().invoke(apio_verify) + result = CliRunner().invoke(apio_lint) # -- It should return an exit code of 0: success assert result.exit_code == 0, result.output assert "[SUCCESS]" in result.output - assert "iverilog" in result.output + assert "verilator" in result.output def test_ledon_upload(): diff --git a/test-examples/TB/edu-ciaa-fpga/and-gate/and_gate.pcf b/test-examples/TB/edu-ciaa-fpga/and-gate/and_gate.pcf new file mode 100644 index 00000000..e3c1886b --- /dev/null +++ b/test-examples/TB/edu-ciaa-fpga/and-gate/and_gate.pcf @@ -0,0 +1,3 @@ +set_io --warn-no-port s 1 #-- output +set_io --warn-no-port a 31 # input +set_io --warn-no-port b 32 # input \ No newline at end of file diff --git a/test-examples/TB/edu-ciaa-fpga/and-gate/and_gate.sv b/test-examples/TB/edu-ciaa-fpga/and-gate/and_gate.sv new file mode 100644 index 00000000..aeaf77b7 --- /dev/null +++ b/test-examples/TB/edu-ciaa-fpga/and-gate/and_gate.sv @@ -0,0 +1,9 @@ +module and_gate ( + input logic a, + input logic b, + output logic s +); + + assign s = a & b; + +endmodule \ No newline at end of file diff --git a/test-examples/TB/edu-ciaa-fpga/and-gate/and_gate_tb.gtkw b/test-examples/TB/edu-ciaa-fpga/and-gate/and_gate_tb.gtkw new file mode 100644 index 00000000..17ea4f31 --- /dev/null +++ b/test-examples/TB/edu-ciaa-fpga/and-gate/and_gate_tb.gtkw @@ -0,0 +1,24 @@ +[*] +[*] GTKWave Analyzer v3.4.0 (w)1999-2022 BSI +[*] Wed Dec 18 04:17:41 2024 +[*] +[dumpfile] "/Volumes/projects/apio-dev/repo/test-examples/TB/edu-ciaa-fpga/and-gate/_build/and_gate_tb.vcd" +[dumpfile_mtime] "Wed Dec 18 04:17:00 2024" +[dumpfile_size] 38921 +[savefile] "/Volumes/projects/apio-dev/repo/test-examples/TB/edu-ciaa-fpga/and-gate/and_gate_tb.gtkw" +[timestart] 0 +[size] 1000 600 +[pos] -1 -1 +*-20.251245 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 +[treeopen] and_gate_testbench. +[sst_width] 253 +[signals_width] 78 +[sst_expanded] 1 +[sst_vpaned_height] 334 +@28 +and_gate_testbench.a +and_gate_testbench.b +@29 +and_gate_testbench.s +[pattern_trace] 1 +[pattern_trace] 0 diff --git a/test-examples/TB/edu-ciaa-fpga/and-gate/and_gate_tb.sv b/test-examples/TB/edu-ciaa-fpga/and-gate/and_gate_tb.sv new file mode 100644 index 00000000..f99ceb58 --- /dev/null +++ b/test-examples/TB/edu-ciaa-fpga/and-gate/and_gate_tb.sv @@ -0,0 +1,28 @@ +`default_nettype none +`timescale 100 ns / 10 ns + +module and_gate_testbench(); + + logic a, b, s; + + // Instantiate device under test + and_gate dut(a, b, s); + + // Apply inputs one at a time + initial begin + $dumpvars(0, and_gate_testbench); + + a = 0; b = 0; + #10; + a = 0; b = 1; + #10; + a = 1; b = 0; + #10; + a = 1; b = 1; + #10; + + $display("End of simulation"); + $finish; + + end +endmodule \ No newline at end of file diff --git a/test-examples/TB/edu-ciaa-fpga/and-gate/apio.ini b/test-examples/TB/edu-ciaa-fpga/and-gate/apio.ini new file mode 100644 index 00000000..f78787a7 --- /dev/null +++ b/test-examples/TB/edu-ciaa-fpga/and-gate/apio.ini @@ -0,0 +1,7 @@ +# APIO project configuration file. For details see +# https://github.com/FPGAwars/apio/wiki/Project-configuration-file + +[env] +board = edu-ciaa-fpga +top-module = and_gate + diff --git a/test-examples/TB/edu-ciaa-fpga/fdd/apio.ini b/test-examples/TB/edu-ciaa-fpga/fdd/apio.ini new file mode 100644 index 00000000..2487063c --- /dev/null +++ b/test-examples/TB/edu-ciaa-fpga/fdd/apio.ini @@ -0,0 +1,6 @@ +# APIO project configuration file. For details see +# https://github.com/FPGAwars/apio/wiki/Project-configuration-file + +[env] +board = edu-ciaa-fpga +top-module = ffd diff --git a/test-examples/TB/edu-ciaa-fpga/fdd/ffd.pcf b/test-examples/TB/edu-ciaa-fpga/fdd/ffd.pcf new file mode 100644 index 00000000..ffd03300 --- /dev/null +++ b/test-examples/TB/edu-ciaa-fpga/fdd/ffd.pcf @@ -0,0 +1,8 @@ +#------ User LEDs ------------------------------------------------------------- +set_io --warn-no-port q 1 # output + +# ------------ User push buttons ---------------------------------------------- +set_io --warn-no-port d 31 # input + +# -------------------------- SYSTEM CLOCK ------------------------------------- +set_io --warn-no-port clk 94 # input \ No newline at end of file diff --git a/test-examples/TB/edu-ciaa-fpga/fdd/ffd.sv b/test-examples/TB/edu-ciaa-fpga/fdd/ffd.sv new file mode 100644 index 00000000..fa657dba --- /dev/null +++ b/test-examples/TB/edu-ciaa-fpga/fdd/ffd.sv @@ -0,0 +1,10 @@ +module ffd( + input logic clk, + input logic d, + output logic q +); + + always_ff @(posedge clk) + q <= d; + +endmodule \ No newline at end of file diff --git a/test-examples/TB/edu-ciaa-fpga/fdd/ffd_tb.gtkw b/test-examples/TB/edu-ciaa-fpga/fdd/ffd_tb.gtkw new file mode 100644 index 00000000..1c0efdb2 --- /dev/null +++ b/test-examples/TB/edu-ciaa-fpga/fdd/ffd_tb.gtkw @@ -0,0 +1,28 @@ +[*] +[*] GTKWave Analyzer v3.4.0 (w)1999-2022 BSI +[*] Wed Dec 18 05:03:12 2024 +[*] +[dumpfile] "/Volumes/projects/apio-dev/repo/test-examples/TB/edu-ciaa-fpga/fdd/_build/ffd_tb.vcd" +[dumpfile_mtime] "Wed Dec 18 05:02:49 2024" +[dumpfile_size] 5281 +[savefile] "/Volumes/projects/apio-dev/repo/test-examples/TB/edu-ciaa-fpga/fdd/ffd_tb.gtkw" +[timestart] 0 +[size] 1000 600 +[pos] -1 -1 +*-21.710676 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 +[treeopen] testbench. +[sst_width] 253 +[signals_width] 95 +[sst_expanded] 1 +[sst_vpaned_height] 158 +@28 +testbench.clk +testbench.d +@420 +testbench.i +@28 +testbench.i_b[1:0] +@29 +testbench.q +[pattern_trace] 1 +[pattern_trace] 0 diff --git a/test-examples/TB/edu-ciaa-fpga/fdd/ffd_tb.sv b/test-examples/TB/edu-ciaa-fpga/fdd/ffd_tb.sv new file mode 100644 index 00000000..fcc2e4ce --- /dev/null +++ b/test-examples/TB/edu-ciaa-fpga/fdd/ffd_tb.sv @@ -0,0 +1,40 @@ +`default_nettype none +`timescale 100 ns / 10 ns + +module testbench(); + +parameter DURATION = 10; + +logic clk = 0; +always #0.5 clk = ~clk; + +logic d, q; + +ffd UUT ( + .clk(clk), + .d(d), + .q(q) +); + +integer i; +reg [1:0] i_b; + +initial begin + + $dumpvars(0, testbench); + + for (i=0; i<100; i=i+1) + begin + $display ("Current loop # %0d", i); + $display ("Current loop # %0b", i); + + #1 + i_b = i; + d = i_b[0]; + end + + #(DURATION) $display("End of simulation"); + $finish; +end + +endmodule \ No newline at end of file diff --git a/test/commands/test_build.py b/test/commands/test_build.py index 208d2b5e..6b1a90be 100644 --- a/test/commands/test_build.py +++ b/test/commands/test_build.py @@ -2,258 +2,68 @@ Test for the "apio build" command """ +from os import chdir from test.conftest import ApioRunner - -# -- apio build entry point from apio.commands.build import cli as apio_build -# pylint: disable=too-many-statements -def test_errors_without_apio_ini_1(apio_runner: ApioRunner): - """Test: Various errors 1/2. All tests are without apio.ini and without - apio packages installed.""" - - with apio_runner.in_sandbox() as sb: - - # -- Execute "apio build" - result = sb.invoke_apio_cmd(apio_build) - assert result.exit_code != 0, result.output - assert "Info: Project has no apio.ini file" in result.output - assert "Error: insufficient arguments: missing board" in result.output - assert "Error: Missing FPGA" in result.output - - # apio build --board icestick - result = sb.invoke_apio_cmd(apio_build, ["--board", "icestick"]) - assert result.exit_code == 1, result.output - assert "apio packages --install --force oss-cad-suite" in result.output - - # apio build --fpga iCE40-HX1K-VQ100 - result = sb.invoke_apio_cmd(apio_build, ["--fpga", "iCE40-HX1K-VQ100"]) - assert result.exit_code == 1, result.output - assert "apio packages --install --force oss-cad-suite" in result.output - - # apio build --type lp --size 8k --pack cm225:4k - result = sb.invoke_apio_cmd( - apio_build, ["--type", "lp", "--size", "8k", "--pack", "cm225:4k"] - ) - assert result.exit_code == 1, result.output - assert "Error: insufficient arguments" in result.output - - # apio build --board icezum --size 1k - result = sb.invoke_apio_cmd( - apio_build, ["--board", "icezum", "--size", "1k"] - ) - assert result.exit_code != 0, result.output - assert "apio packages --install --force oss-cad-suite" in result.output - - # apio build --board icezum --fpga iCE40-HX1K-TQ144 --type hx - result = sb.invoke_apio_cmd( - apio_build, - [ - "--board", - "icezum", - "--fpga", - "iCE40-HX1K-TQ144", - "--type", - "hx", - ], - ) - assert result.exit_code != 0, result.output - assert "apio packages --install --force oss-cad-suite" in result.output - - # apio build --board icezum --pack tq144 - result = sb.invoke_apio_cmd( - apio_build, ["--board", "icezum", "--pack", "tq144"] - ) - assert result.exit_code != 0, result.output - assert "apio packages --install --force oss-cad-suite" in result.output - - # apio build --fpga iCE40-HX1K-TQ144 --pack tq144 --size 1k - result = sb.invoke_apio_cmd( - apio_build, - ["--fpga", "iCE40-HX1K-TQ144", "--pack", "tq144", "--size", "1k"], - ) - assert result.exit_code != 0, result.output - assert "apio packages --install --force oss-cad-suite" in result.output - - # apio build --fpga iCE40-HX1K-TQ144 --type hx - result = sb.invoke_apio_cmd( - apio_build, ["--fpga", "iCE40-HX1K-TQ144", "--type", "hx"] - ) - assert result.exit_code != 0, result.output - assert "apio packages --install --force oss-cad-suite" in result.output - - # apio build --board icezum --size 8k - result = sb.invoke_apio_cmd( - apio_build, ["--board", "icezum", "--size", "8k"] - ) - assert result.exit_code != 0, result.output - assert ( - "Error: contradictory argument values: 'size' = (1k vs 8k)" - in result.output - ) - - # apio build --board icezum --fpga iCE40-HX1K-TQ144 --type lp - result = sb.invoke_apio_cmd( - apio_build, - [ - "--board", - "icezum", - "--fpga", - "iCE40-HX1K-TQ144", - "--type", - "lp", - ], - ) - assert result.exit_code != 0, result.output - assert ( - "Error: contradictory argument values: 'type' = (hx vs lp)" - in result.output - ) - - # apio build --board icezum --fpga iCE40-HX1K-VQ100 - result = sb.invoke_apio_cmd( - apio_build, ["--board", "icezum", "--fpga", "iCE40-HX1K-VQ100"] - ) - assert result.exit_code != 0, result.output - assert ( - "Error: contradictory argument values: 'fpga' = " - "(iCE40-HX1K-TQ144 vs iCE40-HX1K-VQ100)" in result.output - ) - - # apio build --fpga iCE40-HX1K-TQ144 --type lp --size 8k - result = sb.invoke_apio_cmd( - apio_build, - ["--fpga", "iCE40-HX1K-TQ144", "--type", "lp", "--size", "8k"], - ) - assert result.exit_code != 0, result.output - assert ( - "Error: contradictory argument values: 'type' = (hx vs lp)" - in result.output - ) - - # apio build --fpga iCE40-HX1K-TQ144 --pack vq100 - result = sb.invoke_apio_cmd( - apio_build, ["--fpga", "iCE40-HX1K-TQ144", "--pack", "vq100"] - ) - assert result.exit_code != 0, result.output - assert ( - "Error: contradictory argument values: 'pack' = (tq144 vs vq100)" - in result.output - ) - - # apio build --board icezum --pack vq100 - result = sb.invoke_apio_cmd( - apio_build, ["--board", "icezum", "--pack", "vq100"] - ) - assert result.exit_code != 0, result.output - assert ( - "Error: contradictory argument values: 'pack' = (tq144 vs vq100)" - in result.output - ) - - # apio build --size 8k - result = sb.invoke_apio_cmd(apio_build, ["--size", "8k"]) - assert result.exit_code != 0, result.output - assert "Error: insufficient arguments" in result.output - +# R0801: Similar lines in 2 files +# pylint: disable=R0801 +def test_build_without_apio_init(apio_runner: ApioRunner): + """Tests build with various valid and invalid apio variation, all tests + are offline and without any apio package installed.""" -def test_errors_without_apio_ini_2(apio_runner: ApioRunner): - """Test: Various errors 2/2. All tests are without apio.ini and without - apio packages installed.""" with apio_runner.in_sandbox() as sb: - # apio build --type lp - result = sb.invoke_apio_cmd(apio_build, ["--type", "lp"]) - assert result.exit_code != 0, result.output - assert "Error: insufficient arguments" in result.output - - # apio build --type lp --size 8k - result = sb.invoke_apio_cmd( - apio_build, ["--type", "lp", "--size", "8k"] - ) - assert result.exit_code != 0, result.output - assert "Error: insufficient arguments" in result.output - - # apio build --board icefake - result = sb.invoke_apio_cmd(apio_build, ["--board", "icefake"]) - assert result.exit_code != 0, result.output - assert "Error: unknown board: icefake" in result.output - - # apio build --board icefake --fpga iCE40-HX1K-TQ144 - result = sb.invoke_apio_cmd( - apio_build, ["--board", "icefake", "--fpga", "iCE40-HX1K-TQ144"] - ) - assert result.exit_code != 0, result.output - assert "Error: unknown board: icefake" in result.output - - # apio build --fpga iCE40-FAKE - result = sb.invoke_apio_cmd(apio_build, ["--fpga", "iCE40-FAKE"]) - assert result.exit_code != 0, result.output - assert "Error: unknown FPGA: iCE40-FAKE" in result.output - - # apio build --fpga iCE40-FAKE --size 8k - result = sb.invoke_apio_cmd( - apio_build, ["--fpga", "iCE40-FAKE", "--size", "8k"] - ) - assert result.exit_code != 0, result.output - assert "Error: unknown FPGA: iCE40-FAKE" in result.output + # -- Create and change to project dir. + sb.proj_dir.mkdir() + chdir(sb.proj_dir) - # apio build --board icezum --fpga iCE40-FAKE - result = sb.invoke_apio_cmd( - apio_build, ["--board", "icezum", "--fpga", "iCE40-FAKE"] - ) + # -- Run "apio build" without apio.ini + result = sb.invoke_apio_cmd(apio_build) assert result.exit_code != 0, result.output - assert ( - "Error: contradictory argument values: 'fpga' = " - "(iCE40-HX1K-TQ144 vs iCE40-FAKE)" in result.output - ) + assert "Error: missing project file apio.ini" in result.output -def test_errors_with_apio_ini(apio_runner: ApioRunner): - """Test: apio build with apio create""" +def test_build_with_apio_init(apio_runner: ApioRunner): + """Tests build with various valid and invalid apio variation, all tests + are offline and without any apio package installed.""" with apio_runner.in_sandbox() as sb: - # -- Write apio.ini - sb.write_apio_ini({"board": "icezum", "top-module": "main"}) + # -- Create and change to project dir. + sb.proj_dir.mkdir() + chdir(sb.proj_dir) - # apio build - result = sb.invoke_apio_cmd(apio_build) - assert result.exit_code != 0, result.output + # -- Run "apio build" with a valid apio. + sb.write_apio_ini({"board": "alhambra-ii", "top-module": "main"}) + result = sb.invoke_apio_cmd(apio_build, []) + assert result.exit_code == 1, result.output + assert "'oss-cad-suite' is not installed" in result.output - # apio build --board icestick - result = sb.invoke_apio_cmd(apio_build, ["--board", "icestick"]) - assert result.exit_code != 0, result.output - assert ( - "Info: ignoring board specification from apio.ini." - in result.output - ) + # -- Run "apio build" with a missing board var. + sb.write_apio_ini({"top-module": "main"}) + result = sb.invoke_apio_cmd(apio_build, []) + assert result.exit_code == 1, result.output + assert "missing option 'board'" in result.output - # apio build --fpga iCE40-HX1K-VQ100 - result = sb.invoke_apio_cmd(apio_build, ["--fpga", "iCE40-HX1K-VQ100"]) - assert result.exit_code != 0, result.output - assert ( - "Error: contradictory argument values: 'fpga' = " - "(iCE40-HX1K-TQ144 vs iCE40-HX1K-VQ100)" in result.output - ) + # -- Run "apio build" with an invalid board + sb.write_apio_ini({"board": "no-such-board", "top-module": "main"}) + result = sb.invoke_apio_cmd(apio_build, []) + assert result.exit_code == 1, result.output + assert "no such board 'no-such-board'" in result.output - # apio build --type lp --size 8k --pack cm225:4k - result = sb.invoke_apio_cmd( - apio_build, ["--type", "lp", "--size", "8k", "--pack", "cm225:4k"] - ) - assert result.exit_code != 0, result.output - assert ( - "Error: contradictory argument values: 'type' = (hx vs lp)" - in result.output + # -- Run "apio build" with an unknown option. + sb.write_apio_ini( + {"board": "alhambra-ii", "top-module": "main", "unknown": "xyz"} ) + result = sb.invoke_apio_cmd(apio_build, []) + assert result.exit_code == 1, result.output + assert "unknown project option 'unknown'" in result.output - # apio build --type lp --size 8k - result = sb.invoke_apio_cmd( - apio_build, ["--type", "lp", "--size", "8k"] - ) - assert result.exit_code != 0, result.output - assert ( - "Error: contradictory argument values: 'type' = (hx vs lp)" - in result.output - ) + # -- Run "apio build" with no 'top-module' option. + sb.write_apio_ini({"board": "alhambra-ii"}) + result = sb.invoke_apio_cmd(apio_build, []) + assert result.exit_code == 1, result.output + assert "Project file has no 'top-module'" in result.output + assert "package 'oss-cad-suite' is not installed" in result.output diff --git a/test/commands/test_clean.py b/test/commands/test_clean.py index 79bc9ff8..87b0067d 100644 --- a/test/commands/test_clean.py +++ b/test/commands/test_clean.py @@ -2,84 +2,53 @@ Test for the "apio clean" command """ +from os import chdir from pathlib import Path from test.conftest import ApioRunner - - -# -- apio clean entry point from apio.commands.clean import cli as apio_clean -def test_clean_no_apio_ini_no_params(apio_runner: ApioRunner): - """Test: apio clean when no apio.ini file is given - No additional parameters are given - """ +def test_clean_without_apio_ini(apio_runner: ApioRunner): + """Tests the apio clean command without an apio.ini file.""" with apio_runner.in_sandbox() as sb: - # -- Execute "apio clean" - result = sb.invoke_apio_cmd(apio_clean) + # -- Create and change to project dir. + sb.proj_dir.mkdir() + chdir(sb.proj_dir) - # -- It is an error. Exit code should not be 0 + # -- Run "apio clean" with no apio.ini + result = sb.invoke_apio_cmd(apio_clean) assert result.exit_code != 0, result.output - assert "Info: Project has no apio.ini file" in result.output - assert "Error: insufficient arguments: missing board" in result.output - - # -- Execute "apio clean --board alhambra-ii" - result = sb.invoke_apio_cmd(apio_clean, ["--board", "alhambra-ii"]) - assert result.exit_code == 0, result.output + assert "Error: missing project file apio.ini" in result.output -def test_clean_no_apio_ini_params(apio_runner: ApioRunner): - """Test: apio clean when no apio.ini file is given. Board definition - comes from --board parameter. - """ +def test_clean_with_apio_ini(apio_runner: ApioRunner): + """Tests the apio clean command with an apio.ini file.""" with apio_runner.in_sandbox() as sb: - # -- Create a legacy artifact file. - Path("main_tb.vcd").touch() - - # -- Create a current artifact file. - Path("_build").mkdir() - Path("_build/main_tb.vcd").touch() - - # Confirm that the files exists - assert Path("main_tb.vcd").is_file() - assert Path("_build/main_tb.vcd").is_file() + # -- Create and change to project dir. + sb.proj_dir.mkdir() + chdir(sb.proj_dir) - # -- Execute "apio clean --board alhambra-ii" - result = sb.invoke_apio_cmd(apio_clean, ["--board", "alhambra-ii"]) + # -- Run "apio clean" with a valid apio.ini and no dirty files. + sb.write_apio_ini({"board": "alhambra-ii", "top-module": "main"}) + result = sb.invoke_apio_cmd(apio_clean) assert result.exit_code == 0, result.output - # Confirm that the files do not exist. - assert not Path("main_tb.vcd").exists() - assert not Path("_build/main_tb.vcd").exists() - - -def test_clean_create(apio_runner: ApioRunner): - """Test: apio clean when there is an apio.ini file""" - - with apio_runner.in_sandbox() as sb: - - # -- Create apio.ini - sb.write_apio_ini({"board": "icezum", "top-module": "main"}) - - # -- Create a legacy artifact file. - Path("main_tb.vcd").touch() - - # -- Create a current artifact file. - Path("_build").mkdir() - Path("_build/main_tb.vcd").touch() - - # Confirm that the files exists - assert Path("main_tb.vcd").is_file() - assert Path("_build/main_tb.vcd").is_file() - - # --- Execute "apio clean" + # -- Run "apio clean" with apio.ini and dirty files. + sb.write_apio_ini({"board": "alhambra-ii", "top-module": "main"}) + sb.write_file(".sconsign.dblite", "dummy text") + sb.write_file("_build/hardware.out", "dummy text") + assert Path(".sconsign.dblite").exists() + assert Path("_build/hardware.out").exists() + assert Path("_build").exists() result = sb.invoke_apio_cmd(apio_clean) assert result.exit_code == 0, result.output - - # Confirm that the files do not exist. - assert not Path("main_tb.vcd").exists() - assert not Path("_build/main_tb.vcd").exists() + assert "Removed .sconsign.dblite" in result.output + assert "Removed _build/hardware.out" in result.output + assert "Removed directory _build" in result.output + assert not Path(".sconsign.dblite").exists() + assert not Path("_build/hardware.out").exists() + assert not Path("_build").exists() diff --git a/test/commands/test_examples.py b/test/commands/test_examples.py index 617c01a9..fd3ca4c6 100644 --- a/test/commands/test_examples.py +++ b/test/commands/test_examples.py @@ -24,17 +24,14 @@ def test_examples(apio_runner: ApioRunner): # -- Execute "apio examples --list" result = sb.invoke_apio_cmd(apio_examples, ["--list"]) assert result.exit_code == 1, result.output - assert "Error: package 'examples' is not installed" in result.output - assert "apio packages --install --force examples" in result.output + assert "package 'examples' is not installed" in result.output # -- Execute "apio examples --fetch-dir dir" result = sb.invoke_apio_cmd(apio_examples, ["--fetch-dir", "dir"]) assert result.exit_code == 1, result.output - assert "Error: package 'examples' is not installed" in result.output - assert "apio packages --install --force examples" in result.output + assert "package 'examples' is not installed" in result.output # -- Execute "apio examples --files file" result = sb.invoke_apio_cmd(apio_examples, ["--fetch-files", "file"]) assert result.exit_code == 1, result.output - assert "Error: package 'examples' is not installed" in result.output - assert "apio packages --install --force examples" in result.output + assert "package 'examples' is not installed" in result.output diff --git a/test/commands/test_graph.py b/test/commands/test_graph.py index e24e787c..9dbf527c 100644 --- a/test/commands/test_graph.py +++ b/test/commands/test_graph.py @@ -2,21 +2,26 @@ Test for the "apio graph" command """ +from os import chdir from test.conftest import ApioRunner - -# -- apio graph entry point from apio.commands.graph import cli as apio_graph +# R0801: Similar lines in 2 files +# pylint: disable=R0801 def test_graph_no_apio_ini(apio_runner: ApioRunner): """Test: apio graph with no apio.ini""" with apio_runner.in_sandbox() as sb: + # -- Create and change to project dir. + sb.proj_dir.mkdir() + chdir(sb.proj_dir) + # -- Execute "apio graph" result = sb.invoke_apio_cmd(apio_graph) assert result.exit_code == 1, result.output - assert "Error: insufficient arguments: missing board" in result.output + assert "Error: missing project file apio.ini" in result.output def test_graph_with_apio_ini(apio_runner: ApioRunner): @@ -24,6 +29,10 @@ def test_graph_with_apio_ini(apio_runner: ApioRunner): with apio_runner.in_sandbox() as sb: + # -- Create and change to project dir. + sb.proj_dir.mkdir() + chdir(sb.proj_dir) + # -- Create an apio.ini file sb.write_apio_ini({"board": "icezum", "top-module": "main"}) @@ -31,16 +40,13 @@ def test_graph_with_apio_ini(apio_runner: ApioRunner): result = sb.invoke_apio_cmd(apio_graph) assert result.exit_code == 1, result.output assert "package 'oss-cad-suite' is not installed" in result.output - assert "apio packages --install --force oss-cad-suite" in result.output # -- Execute "apio graph -pdf" result = sb.invoke_apio_cmd(apio_graph) assert result.exit_code == 1, result.output assert "package 'oss-cad-suite' is not installed" in result.output - assert "apio packages --install --force oss-cad-suite" in result.output # -- Execute "apio graph -png" result = sb.invoke_apio_cmd(apio_graph) assert result.exit_code == 1, result.output assert "package 'oss-cad-suite' is not installed" in result.output - assert "apio packages --install --force oss-cad-suite" in result.output diff --git a/test/commands/test_install.py b/test/commands/test_install.py deleted file mode 100644 index 822af7e5..00000000 --- a/test/commands/test_install.py +++ /dev/null @@ -1,27 +0,0 @@ -""" - Test for the "apio install" command -""" - -from test.conftest import ApioRunner - -# -- apio install entry point -from apio.commands.install import cli as apio_install - - -def test_install(apio_runner: ApioRunner): - """Test "apio install" with different parameters""" - - with apio_runner.in_sandbox() as sb: - - # -- Execute "apio install" - result = sb.invoke_apio_cmd(apio_install) - sb.assert_ok(result) - - # -- Execute "apio install --list" - result = sb.invoke_apio_cmd(apio_install, ["--list"]) - sb.assert_ok(result) - - # -- Execute "apio install missing_package" - result = sb.invoke_apio_cmd(apio_install, ["missing_package"]) - assert result.exit_code == 1, result.output - assert "Error: no such package" in result.output diff --git a/test/commands/test_lint.py b/test/commands/test_lint.py index db84c163..ee23e159 100644 --- a/test/commands/test_lint.py +++ b/test/commands/test_lint.py @@ -19,7 +19,4 @@ def test_lint_no_packages(apio_runner: ApioRunner): # -- Execute "apio lint" result = sb.invoke_apio_cmd(apio_lint) assert result.exit_code == 1, result.output - assert ( - "Error: package 'oss-cad-suite' is not installed" in result.output - ) - assert "apio packages --install --force oss-cad-suite" in result.output + assert "package 'oss-cad-suite' is not installed" in result.output diff --git a/test/commands/test_modify.py b/test/commands/test_modify.py deleted file mode 100644 index f7cffa1a..00000000 --- a/test/commands/test_modify.py +++ /dev/null @@ -1,109 +0,0 @@ -""" - Test for the "apio modify" command -""" - -from pathlib import Path -from os.path import isfile, exists -from typing import Dict -from test.conftest import ApioRunner -from configobj import ConfigObj - - -# -- apio modify entry point -from apio.commands.modify import cli as apio_modify - - -# R0801: Similar lines in 2 files -# pylint: disable=R0801 -def check_ini_file(apio_ini: Path, expected_vars: Dict[str, str]) -> None: - """Assert that apio.ini contains exactly the given vars.""" - # Read the ini file. - assert isfile(apio_ini) - conf = ConfigObj(str(apio_ini)) - # Check the expected comment at the top. - assert "# My initial comment." in conf.initial_comment[0] - # Check the expected vars. - assert conf.dict() == {"env": expected_vars} - - -def test_modify(apio_runner: ApioRunner): - """Test "apio modify" with different parameters""" - - with apio_runner.in_sandbox() as sb: - - apio_ini = Path("apio.ini") - assert not exists(apio_ini) - - # -- Execute "apio modify --top-module my_module" - result = sb.invoke_apio_cmd(apio_modify, ["--top-module", "my_module"]) - assert result.exit_code != 0, result.output - assert "Error: 'apio.ini' not found" in result.output - assert not exists(apio_ini) - - # -- Create initial apio.ini file. - conf = ConfigObj(str(apio_ini)) - conf.initial_comment = ["# My initial comment.", ""] - conf["env"] = { - "board": "icezum", - "top-module": "my_module", - "extra_var": "dummy", - } - conf.write() - check_ini_file( - apio_ini, - { - "board": "icezum", - "top-module": "my_module", - "extra_var": "dummy", - }, - ) - - # -- Execute "apio modify --board missed_board" - result = sb.invoke_apio_cmd(apio_modify, ["--board", "missed_board"]) - assert result.exit_code == 1, result.output - assert "Error: no such board" in result.output - check_ini_file( - apio_ini, - { - "board": "icezum", - "top-module": "my_module", - "extra_var": "dummy", - }, - ) - - # -- Execute "apio modify --board alhambra-ii" - result = sb.invoke_apio_cmd(apio_modify, ["--board", "alhambra-ii"]) - sb.assert_ok(result) - assert "was modified successfully." in result.output - check_ini_file( - apio_ini, - { - "board": "alhambra-ii", - "top-module": "my_module", - "extra_var": "dummy", - }, - ) - - # -- Execute "apio modify --top-module my_main" - result = sb.invoke_apio_cmd(apio_modify, ["--top-module", "my_main"]) - sb.assert_ok(result) - assert "was modified successfully." in result.output - check_ini_file( - apio_ini, - { - "board": "alhambra-ii", - "top-module": "my_main", - "extra_var": "dummy", - }, - ) - - # -- Execute "apio modify --board icezum --top-module my_top" - result = sb.invoke_apio_cmd( - apio_modify, ["--board", "icezum", "--top-module", "my_top"] - ) - sb.assert_ok(result) - assert "was modified successfully." in result.output - check_ini_file( - apio_ini, - {"board": "icezum", "top-module": "my_top", "extra_var": "dummy"}, - ) diff --git a/test/commands/test_report.py b/test/commands/test_report.py index 347f936e..97de9b1d 100644 --- a/test/commands/test_report.py +++ b/test/commands/test_report.py @@ -2,6 +2,7 @@ Test for the "apio report" command """ +from os import chdir from test.conftest import ApioRunner # -- apio report entry point @@ -10,33 +11,32 @@ # R0801: Similar lines in 2 files # pylint: disable=R0801 -def test_report(apio_runner: ApioRunner): - """Test: apio report - when no apio.ini file is given - No additional parameters are given - """ +def test_report_no_apio(apio_runner: ApioRunner): + """Tests the apio report command without an apio.ini file.""" with apio_runner.in_sandbox() as sb: - # -- Execute "apio report" - result = sb.invoke_apio_cmd(apio_report) + # -- Create and change to project dir. + sb.proj_dir.mkdir() + chdir(sb.proj_dir) - # -- Check the result + # -- Run "apio report" without apio.ini + result = sb.invoke_apio_cmd(apio_report) assert result.exit_code != 0, result.output - assert "Info: Project has no apio.ini file" in result.output - assert "Error: insufficient arguments: missing board" in result.output + assert "Error: missing project file apio.ini" in result.output -def test_report_board(apio_runner: ApioRunner): - """Test: apio report - when parameters are given - """ +def test_report_with_apio(apio_runner: ApioRunner): + """Tests the apio report command with an apio.ini file.""" with apio_runner.in_sandbox() as sb: - # -- Execute "apio report" - result = sb.invoke_apio_cmd(apio_report, ["--board", "icezum"]) + # -- Create and change to project dir. + sb.proj_dir.mkdir() + chdir(sb.proj_dir) - # -- Check the result + # -- Run "apio report" with apio.ini + sb.write_apio_ini({"board": "alhambra-ii", "top-module": "main"}) + result = sb.invoke_apio_cmd(apio_report) assert result.exit_code != 0, result.output - assert "apio packages --install --force oss-cad-suite" in result.output + assert "package 'oss-cad-suite' is not installed" in result.output diff --git a/test/commands/test_system.py b/test/commands/test_system.py index b6330ab9..65280751 100644 --- a/test/commands/test_system.py +++ b/test/commands/test_system.py @@ -24,24 +24,21 @@ def test_system(apio_runner: ApioRunner): # -- Execute "apio system --lsftdi" result = sb.invoke_apio_cmd(apio_system, ["--lsftdi"]) assert result.exit_code == 1, result.output - assert "apio packages --install --force oss-cad-suite" in result.output + assert "package 'oss-cad-suite' is not installed" in result.output # -- Execute "apio system --lsusb" result = sb.invoke_apio_cmd(apio_system, ["--lsusb"]) assert result.exit_code == 1, result.output - assert "apio packages --install --force oss-cad-suite" in result.output + assert "package 'oss-cad-suite' is not installed" in result.output # -- Execute "apio system --lsserial" sb.invoke_apio_cmd(apio_system, ["--lsserial"]) assert result.exit_code == 1, result.output - assert "apio packages --install --force oss-cad-suite" in result.output + assert "package 'oss-cad-suite' is not installed" in result.output # -- Execute "apio system --info" result = sb.invoke_apio_cmd(apio_system, ["--info"]) assert result.exit_code == 0, result.output assert "Platform id" in result.output # -- The these env options are set by the apio text fixture. - assert ( - "Active env options [APIO_HOME_DIR, APIO_PACKAGES_DIR]" - in result.output - ) + assert "Active env options [APIO_HOME_DIR]" in result.output diff --git a/test/commands/test_time.py b/test/commands/test_time.py deleted file mode 100644 index c31a4f7a..00000000 --- a/test/commands/test_time.py +++ /dev/null @@ -1,42 +0,0 @@ -""" - Test for the "apio time" command -""" - -from test.conftest import ApioRunner - -# -- apio time entry point -from apio.commands.time import cli as apio_time - - -# R0801: Similar lines in 2 files -# pylint: disable=R0801 -def test_time(apio_runner: ApioRunner): - """Test: apio time - when no apio.ini file is given - No additional parameters are given - """ - - with apio_runner.in_sandbox() as sb: - - # -- Execute "apio time" - result = sb.invoke_apio_cmd(apio_time) - - # -- Check the result - assert result.exit_code != 0, result.output - assert "Info: Project has no apio.ini file" in result.output - assert "Error: insufficient arguments: missing board" in result.output - - -def test_time_board(apio_runner: ApioRunner): - """Test: apio time - when parameters are given - """ - - with apio_runner.in_sandbox() as sb: - - # -- Execute "apio time" - result = sb.invoke_apio_cmd(apio_time, ["--board", "icezum"]) - - # -- Check the result - assert result.exit_code != 0, result.output - assert "apio packages --install --force oss-cad-suite" in result.output diff --git a/test/commands/test_uninstall.py b/test/commands/test_uninstall.py deleted file mode 100644 index 803dd362..00000000 --- a/test/commands/test_uninstall.py +++ /dev/null @@ -1,30 +0,0 @@ -""" - Test for the "apio uninstall" command -""" - -from test.conftest import ApioRunner - -# -- apio uninstall entry point -from apio.commands.uninstall import cli as apio_uninstall - - -def test_uninstall(apio_runner: ApioRunner): - """Test "apio uninstall" with different parameters""" - - with apio_runner.in_sandbox() as sb: - - # -- Execute "apio uninstall" - result = sb.invoke_apio_cmd(apio_uninstall) - sb.assert_ok(result) - - # -- Execute "apio uninstall --list" - result = sb.invoke_apio_cmd(apio_uninstall, ["--list"]) - sb.assert_ok(result) - - # -- Execute "apio uninstall missing_packge" - result = sb.invoke_apio_cmd( - apio_uninstall, ["missing_package"], input="y" - ) - assert result.exit_code == 1, result.output - assert "Do you want to uninstall?" in result.output - assert "Error: no such package" in result.output diff --git a/test/commands/test_upload.py b/test/commands/test_upload.py index 06628815..d4972e3e 100644 --- a/test/commands/test_upload.py +++ b/test/commands/test_upload.py @@ -2,13 +2,14 @@ Test for the "apio upload" command """ +from os import chdir from test.conftest import ApioRunner - -# -- apio time entry point from apio.commands.upload import cli as apio_upload -def test_upload(apio_runner: ApioRunner): +# R0801: Similar lines in 2 files +# pylint: disable=R0801 +def test_upload_without_apio_ini(apio_runner: ApioRunner): """Test: apio upload when no apio.ini file is given No additional parameters are given @@ -16,30 +17,16 @@ def test_upload(apio_runner: ApioRunner): with apio_runner.in_sandbox() as sb: + # -- Create and change to project dir. + sb.proj_dir.mkdir() + chdir(sb.proj_dir) + # -- Execute "apio upload" result = sb.invoke_apio_cmd(apio_upload) # -- Check the result assert result.exit_code == 1, result.output - assert "Info: Project has no apio.ini file" in result.output - assert "Error: insufficient arguments: missing board" in result.output - - -def test_upload_board(apio_runner: ApioRunner): - """Test: apio upload --board icezum - No oss-cad-suite package is installed - """ - - with apio_runner.in_sandbox() as sb: - - # -- Execute "apio upload --board icezum" - result = sb.invoke_apio_cmd(apio_upload, ["--board", "icezum"]) - - # -- Check the result - assert result.exit_code == 1 - assert ( - "Error: package 'oss-cad-suite' is not installed" in result.output - ) + assert "Error: missing project file apio.ini" in result.output def test_upload_complete(apio_runner: ApioRunner): @@ -49,47 +36,22 @@ def test_upload_complete(apio_runner: ApioRunner): with apio_runner.in_sandbox() as sb: + # -- Create and change to project dir. + sb.proj_dir.mkdir() + chdir(sb.proj_dir) + # -- Execute "apio upload --serial-port COM0" + sb.write_apio_ini({"board": "alhambra-ii", "top-module": "main"}) result = sb.invoke_apio_cmd(apio_upload, ["--serial-port", "COM0"]) assert result.exit_code == 1, result.output - assert "Info: Project has no apio.ini file" in result.output - assert "Error: insufficient arguments: missing board" in result.output + assert "package 'oss-cad-suite' is not installed" in result.output # -- Execute "apio upload --ftdi-id 0" result = sb.invoke_apio_cmd(apio_upload, ["--ftdi-id", "0"]) assert result.exit_code == 1, result.output - assert "Info: Project has no apio.ini file" in result.output - assert "Error: insufficient arguments: missing board" in result.output + assert "package 'oss-cad-suite' is not installed" in result.output # -- Execute "apio upload --sram" result = sb.invoke_apio_cmd(apio_upload, ["--sram"]) assert result.exit_code == 1, result.output - assert "Info: Project has no apio.ini file" in result.output - assert "Error: insufficient arguments: missing board" in result.output - - # -- Execute "apio upload --board icezum --serial-port COM0" - result = sb.invoke_apio_cmd( - apio_upload, ["--board", "icezum", "--serial-port", "COM0"] - ) - assert result.exit_code == 1, result.output - assert ( - "Error: package 'oss-cad-suite' is not installed" in result.output - ) - - # -- Execute "apio upload --board icezum --ftdi-id 0" - result = sb.invoke_apio_cmd( - apio_upload, ["--board", "icezum", "--ftdi-id", "0"] - ) - assert result.exit_code == 1, result.output - assert ( - "Error: package 'oss-cad-suite' is not installed" in result.output - ) - - # -- Execute "apio upload --board icezum --sram" - result = sb.invoke_apio_cmd( - apio_upload, ["--board", "icezum", "--sram"] - ) - assert result.exit_code == 1, result.output - assert ( - "Error: package 'oss-cad-suite' is not installed" in result.output - ) + assert "package 'oss-cad-suite' is not installed" in result.output diff --git a/test/commands/test_verify.py b/test/commands/test_verify.py deleted file mode 100644 index 2289cc20..00000000 --- a/test/commands/test_verify.py +++ /dev/null @@ -1,24 +0,0 @@ -""" - Test for the "apio verify" command -""" - -from test.conftest import ApioRunner - -# -- apio verify entry point -from apio.commands.verify import cli as apio_verify - - -def test_verify(apio_runner: ApioRunner): - """Test: apio verify - when no apio.ini file is given - No additional parameters are given - """ - - with apio_runner.in_sandbox() as sb: - - # -- Execute "apio verify" - result = sb.invoke_apio_cmd(apio_verify, ["--board", "icezum"]) - - # -- Check the result - assert result.exit_code != 0, result.output - assert "apio packages --install --force oss-cad-suite" in result.output diff --git a/test/conftest.py b/test/conftest.py index 754a7435..b760d00c 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -24,14 +24,9 @@ # -- Debug mode on/off DEBUG = True -# -- Apio should be able to handle spaces and unicode in its home, packages, -# -- and project directory path. We insert this marker in the test pathes to -# -- test it. -# -# -- TODO: Currently apio doesn't handle well spaces in the pathes. Fix it and -# -- change this to " fuññy ". For more details see -# -- https://github.com/FPGAwars/apio/issues/474. -FUNNY_MARKER = " fuññy " +# -- We insert unicode to the test pathes to make sure apio handle them +# -- properly. +FUNNY_MARKER = "fuññy" # -- This function is called by pytest. It addes the pytest --offline flag @@ -59,12 +54,10 @@ def __init__( apio_runner_: "ApioRunner", proj_dir: Path, home_dir: Path, - packages_dir: Path, ): self._apio_runner = apio_runner_ self._proj_dir = proj_dir self._home_dir = home_dir - self._packages_dir = packages_dir self._click_runner = CliRunner() @property @@ -89,8 +82,7 @@ def home_dir(self) -> Path: @property def packages_dir(self) -> Path: """Returns the sandbox's apio packages dir.""" - assert not self.expired, "Sanbox expired" - return self._packages_dir + return self.home_dir / "packages" # R0913: Too many arguments (7/5) (too-many-arguments) # pylint: disable=R0913 @@ -125,7 +117,6 @@ def invoke_apio_cmd( # -- These two env vars are set when creating the context. Let's # -- check that the test didn't corrupt them. assert os.environ["APIO_HOME_DIR"] == str(self.home_dir) - assert os.environ["APIO_PACKAGES_DIR"] == str(self.packages_dir) # -- Invoke the command. Get back the collected results. result = self._click_runner.invoke( @@ -203,6 +194,9 @@ def write_file( if isinstance(text, list): text = "\n".join(text) + # -- Make dir(s) if needed. + Path(file).parent.mkdir(parents=True, exist_ok=True) + # -- Write. with open(file, "w", encoding="utf-8") as f: f.write(text) @@ -242,7 +236,7 @@ def write_apio_ini(self, properties: Dict[str, str]): lines.append(f"{name} = {value}") # -- Write the file. - self.write_file(path, lines) + self.write_file(path, lines, exists_ok=True) @property def offline_flag(self): @@ -300,9 +294,13 @@ def in_sandbox(self): # -- Construct the sandbox dir pathes. User will create the dirs # -- as needed. - proj_dir = temp_dir / "proj" + # -- + # -- We do allow spaces in the project dir. + proj_dir = temp_dir / " proj" + + # -- Spaces are not supported yet in the home and packges dirs. + # -- For more details see https://github.com/FPGAwars/apio/issues/474. home_dir = temp_dir / "apio" - packages_dir = temp_dir / "packages" if DEBUG: print() @@ -310,17 +308,15 @@ def in_sandbox(self): print(f" test dir : {str(temp_dir)}") print(f" apio proj dir : {str(proj_dir)}") print(f" apio home dir : {str(home_dir)}") - print(f" apio packages dir : {str(packages_dir)}") print() # -- Register a sanbox objet to indicate that we are in a sandbox. assert self._sandbox is None - self._sandbox = ApioSandbox(self, proj_dir, home_dir, packages_dir) + self._sandbox = ApioSandbox(self, proj_dir, home_dir) # -- Set the system env vars to inform ApioContext what are the # -- home and packages dirs. os.environ["APIO_HOME_DIR"] = str(home_dir) - os.environ["APIO_PACKAGES_DIR"] = str(packages_dir) try: # -- This is the end of the context manager _entry part. The diff --git a/test/integration/test_examples.py b/test/integration/test_examples.py index ca25c6cf..0f55ad83 100644 --- a/test/integration/test_examples.py +++ b/test/integration/test_examples.py @@ -2,12 +2,8 @@ Test different "apio" commands """ -from os import stat from os import chdir from os.path import getsize -from os import path -from os import system -from pathlib import Path from test.conftest import ApioRunner import pytest @@ -32,21 +28,11 @@ def test_examples(apio_runner: ApioRunner): sb.proj_dir.mkdir() chdir(sb.proj_dir) - base_path = Path(sb.packages_dir) - file_path = ( - base_path / "examples" / "Alhambra-II" / "ledon" / "ledon.v" - ) - file_path = path.normpath(str(file_path)) - # -- Install the examples package. result = sb.invoke_apio_cmd(apio_packages, ["--install", "examples"]) sb.assert_ok(result) assert "Package 'examples' installed successfully" in result.output - try: - size = stat(file_path).st_size - assert size > 0 - except FileNotFoundError: - assert False, f"El archivo {file_path} no existe" + assert getsize(sb.packages_dir / "examples/alhambra-ii/ledon/ledon.v") # -- 'apio examples --list' result = sb.invoke_apio_cmd( @@ -54,45 +40,45 @@ def test_examples(apio_runner: ApioRunner): ["--list"], ) sb.assert_ok(result) - assert "Alhambra-II/ledon" in result.output + assert "alhambra-ii/ledon" in result.output assert "Hello world for the Alhambra-II board" in result.output - # -- 'apio examples --fetch-files Alhambra-II/ledon' + # -- 'apio examples --fetch-files alhambra-ii/ledon' result = sb.invoke_apio_cmd( apio_examples, - ["--fetch-files", "Alhambra-II/ledon"], + ["--fetch-files", "alhambra-ii/ledon"], ) sb.assert_ok(result) - assert "Copying Alhambra-II/ledon example files" in result.output + assert "Copying alhambra-ii/ledon example files" in result.output assert "have been successfully created!" in result.output assert getsize("ledon.v") - # -- 'apio examples --fetch-dir Alhambra-II/ledon' + # -- 'apio examples --fetch-dir alhambra-ii/ledon' result = sb.invoke_apio_cmd( apio_examples, - ["--fetch-dir", "Alhambra-II/ledon"], + ["--fetch-dir", "alhambra-ii/ledon"], ) sb.assert_ok(result) - assert "Creating Alhambra-II/ledon directory" in result.output + assert "Creating alhambra-ii/ledon directory" in result.output assert "has been successfully created" in result.output - assert getsize("Alhambra-II/ledon/ledon.v") + assert getsize("alhambra-ii/ledon/ledon.v") - # -- 'apio examples --fetch-files" Alhambra-II/ledon -p dir1' + # -- 'apio examples --fetch-files" alhambra-ii/ledon -p dir1' result = sb.invoke_apio_cmd( apio_examples, - ["--fetch-files", "Alhambra-II/ledon", "-p", "dir1"], + ["--fetch-files", "alhambra-ii/ledon", "-p", "dir1"], ) sb.assert_ok(result) - assert "Copying Alhambra-II/ledon example files" in result.output + assert "Copying alhambra-ii/ledon example files" in result.output assert "have been successfully created!" in result.output assert getsize("dir1/ledon.v") - # -- 'apio examples --fetch-dir Alhambra-II/ledon -p dir2 + # -- 'apio examples --fetch-dir alhambra-ii/ledon -p dir2 result = sb.invoke_apio_cmd( apio_examples, - ["--fetch-dir", "Alhambra-II/ledon", "-p", "dir2"], + ["--fetch-dir", "alhambra-ii/ledon", "-p", "dir2"], ) sb.assert_ok(result) - assert "Creating Alhambra-II/ledon directory" in result.output + assert "Creating alhambra-ii/ledon directory" in result.output assert "has been successfully created" in result.output - assert getsize("dir2/Alhambra-II/ledon/ledon.v") + assert getsize("dir2/alhambra-ii/ledon/ledon.v") diff --git a/test/integration/test_packages.py b/test/integration/test_packages.py index 48b24c8c..3fa8f719 100644 --- a/test/integration/test_packages.py +++ b/test/integration/test_packages.py @@ -43,7 +43,7 @@ def test_packages(apio_runner: ApioRunner): result = sb.invoke_apio_cmd(apio_packages, ["--install", "examples"]) sb.assert_ok(result) assert "Package 'examples' installed successfully" in result.output - assert listdir(sb.packages_dir / "examples/examples/alhambra-ii") + assert listdir(sb.packages_dir / "examples/alhambra-ii") assert "tools-oss-cad-suite" not in listdir(sb.packages_dir) # -- Install the reset of the packages. @@ -55,14 +55,12 @@ def test_packages(apio_runner: ApioRunner): assert ( "Package 'oss-cad-suite' installed successfully" in result.output ) - assert listdir(sb.packages_dir / "examples/examples/alhambra-ii") + assert listdir(sb.packages_dir / "examples/alhambra-ii") assert listdir(sb.packages_dir / "tools-oss-cad-suite/bin") # -- Delete a file from the examples package, we will use it as an # -- indicator for the reinstallation of the package. - marker_file = ( - sb.packages_dir / "examples/examples/alhambra-ii/ledon/ledon.v" - ) + marker_file = sb.packages_dir / "examples/alhambra-ii/ledon/ledon.v" assert marker_file.is_file() marker_file.unlink() assert not marker_file.exists() diff --git a/test/integration/test_projects.py b/test/integration/test_projects.py index 3dcdd6cf..4d0b2cbf 100644 --- a/test/integration/test_projects.py +++ b/test/integration/test_projects.py @@ -19,11 +19,16 @@ from apio.commands.examples import cli as apio_examples +# Too many statements (60/50) (too-many-statements) +# pylint: disable=too-many-statements # R0801: Similar lines in 2 files # pylint: disable=R0801 +# R0913: Too many arguments (6/5) (too-many-arguments) +# pylint: disable=too-many-arguments def _test_project( apio_runner: ApioRunner, *, + remote_proj_dir: bool, example: str, testbench: str, binary: str, @@ -42,10 +47,17 @@ def _test_project( # -- Create and change to project directory. sb.proj_dir.mkdir() - chdir(sb.proj_dir) + if not remote_proj_dir: + chdir(sb.proj_dir) + + # -- In remote project dir mode we don't step into the project dir + # -- and pass it as an arg. + proj_arg = ["-p", str(sb.proj_dir)] if remote_proj_dir else [] # -- 'apio packages --install --verbose' - result = sb.invoke_apio_cmd(apio_packages, ["--install", "--verbose"]) + result = sb.invoke_apio_cmd( + apio_packages, ["--install", "--verbose"] + proj_arg + ) sb.assert_ok(result) assert "'examples' installed successfully" in result.output assert "'oss-cad-suite' installed successfully" in result.output @@ -53,68 +65,91 @@ def _test_project( assert listdir(sb.packages_dir / "tools-oss-cad-suite") # -- the project directory should be empty. - assert not listdir(".") + assert not listdir(sb.proj_dir) # -- 'apio examples --fetch-files `` result = sb.invoke_apio_cmd( apio_examples, - ["--fetch-files", example], + ["--fetch-files", example] + proj_arg, ) sb.assert_ok(result) assert f"Copying {example} example files" in result.output assert "have been successfully created!" in result.output - assert getsize("apio.ini") + assert getsize(sb.proj_dir / "apio.ini") # -- Remember the original list of project files. - project_files = listdir(".") + project_files = listdir(sb.proj_dir) + + # -- 'apio build' + result = sb.invoke_apio_cmd(apio_build, proj_arg) + sb.assert_ok(result) + assert "SUCCESS" in result.output + assert getsize(sb.proj_dir / "_build" / binary) + + # -- 'apio build' (no change) + result = sb.invoke_apio_cmd(apio_build, proj_arg) + sb.assert_ok(result) + assert "SUCCESS" in result.output + assert "yosys" not in result.output + + # -- Modify apio.ini + apio_ini_lines = sb.read_file( + sb.proj_dir / "apio.ini", lines_mode=True + ) + apio_ini_lines.append(" ") + sb.write_file(sb.proj_dir / "apio.ini", apio_ini_lines, exists_ok=True) # -- 'apio build' - result = sb.invoke_apio_cmd(apio_build) + # -- Apio.ini modification should triggers a new build. + result = sb.invoke_apio_cmd(apio_build, proj_arg) sb.assert_ok(result) assert "SUCCESS" in result.output - assert getsize(f"_build/{binary}") + assert "yosys" in result.output # -- 'apio lint' - result = sb.invoke_apio_cmd(apio_lint) + result = sb.invoke_apio_cmd(apio_lint, proj_arg) sb.assert_ok(result) assert "SUCCESS" in result.output - assert getsize("_build/hardware.vlt") + assert getsize(sb.proj_dir / "_build/hardware.vlt") # -- 'apio test' - result = sb.invoke_apio_cmd(apio_test) + result = sb.invoke_apio_cmd(apio_test, proj_arg) sb.assert_ok(result) assert "SUCCESS" in result.output - assert getsize(f"_build/{testbench}.out") - assert getsize(f"_build/{testbench}.vcd") + assert getsize(sb.proj_dir / f"_build/{testbench}.out") + assert getsize(sb.proj_dir / f"_build/{testbench}.vcd") # -- 'apio report' - result = sb.invoke_apio_cmd(apio_report) + result = sb.invoke_apio_cmd(apio_report, proj_arg) sb.assert_ok(result) assert "SUCCESS" in result.output assert report_item in result.output - assert getsize("_build/hardware.pnr") + assert getsize(sb.proj_dir / "_build/hardware.pnr") # -- 'apio graph' - result = sb.invoke_apio_cmd(apio_graph) + result = sb.invoke_apio_cmd(apio_graph, proj_arg) sb.assert_ok(result) assert "SUCCESS" in result.output - assert getsize("_build/hardware.dot") - assert getsize("_build/hardware.svg") + assert getsize(sb.proj_dir / "_build/hardware.dot") + assert getsize(sb.proj_dir / "_build/hardware.svg") # -- 'apio clean' - result = sb.invoke_apio_cmd(apio_clean) + result = sb.invoke_apio_cmd(apio_clean, proj_arg) sb.assert_ok(result) assert "SUCCESS" in result.output - assert not Path("_build").exists() + assert not Path(sb.proj_dir / "_build").exists() # -- Here we should have only the original project files. - assert set(listdir(".")) == set(project_files) + assert set(listdir(sb.proj_dir)) == set(project_files) -def test_project_ice40(apio_runner: ApioRunner): - """Tests building and testing an ice40 project.""" +def test_project_ice40_local_dir(apio_runner: ApioRunner): + """Tests building and testing an ice40 project as the current working + dir.""" + _test_project( apio_runner, + remote_proj_dir=False, example="alhambra-ii/ledon", testbench="ledon_tb", binary="hardware.bin", @@ -122,10 +157,36 @@ def test_project_ice40(apio_runner: ApioRunner): ) -def test_project_ecp5(apio_runner: ApioRunner): - """Tests building and testing an ecp5 project.""" +def test_project_ice40_remote_dir(apio_runner: ApioRunner): + """Tests building and testing an ice40 project from a remote dir, using + the -p option.""" + _test_project( + apio_runner, + remote_proj_dir=True, + example="alhambra-ii/ledon", + testbench="ledon_tb", + binary="hardware.bin", + report_item="ICESTORM_LC:", + ) + + +def test_project_ecp5_local_dir(apio_runner: ApioRunner): + """Tests building and testing an ecp5 project as the current working dir""" + _test_project( + apio_runner, + remote_proj_dir=False, + example="ColorLight-5A-75B-V8/Ledon", + testbench="ledon_tb", + binary="hardware.bit", + report_item="ALU54B:", + ) + + +def test_project_ecp5_remote_dir(apio_runner: ApioRunner): + """Tests building and testing an ecp5 project from a remote directory.""" _test_project( apio_runner, + remote_proj_dir=True, example="ColorLight-5A-75B-V8/Ledon", testbench="ledon_tb", binary="hardware.bit", diff --git a/test/managers/test_scons_args.py b/test/managers/test_scons_args.py index d0452e49..6b970633 100644 --- a/test/managers/test_scons_args.py +++ b/test/managers/test_scons_args.py @@ -14,9 +14,7 @@ def test_arg(): # -- Test an arg that is mapped to an scons varible. arg = Arg("arg1", "var1") assert not arg.has_value - assert not arg.has_var_value arg.set("val1") assert arg.has_value - assert arg.has_var_value assert arg.value == "val1" assert arg.var_name == "var1" diff --git a/test/scons/test_scons_util.py b/test/scons/test_scons_util.py index c644949f..7f0c8bf8 100644 --- a/test/scons/test_scons_util.py +++ b/test/scons/test_scons_util.py @@ -143,7 +143,7 @@ def test_dependencies(apio_runner: ApioRunner): # -- Check the list. The scanner returns the files sorted and # -- with dulicates removed. - assert file_names == ["apio_testing.vh", "v771499.list"] + assert file_names == ["apio.ini", "apio_testing.vh", "v771499.list"] def test_has_testbench_name(): @@ -171,18 +171,36 @@ def test_is_verilog_src(): env = _make_test_scons_env() - # -- Verilog source names + # -- Verilog and system-verilog source names, system-verilog included. assert is_verilog_src(env, "aaa.v") assert is_verilog_src(env, "bbb/aaa.v") assert is_verilog_src(env, "bbb\\aaa.v") assert is_verilog_src(env, "aaatb.v") assert is_verilog_src(env, "aaa_tb.v") - - # -- Non verilog source names. + assert is_verilog_src(env, "aaa.sv") + assert is_verilog_src(env, "bbb\\aaa.sv") + assert is_verilog_src(env, "aaa_tb.sv") + + # -- Verilog and system-verilog source names, system-verilog excluded. + assert is_verilog_src(env, "aaa.v", include_sv=False) + assert is_verilog_src(env, "bbb/aaa.v", include_sv=False) + assert is_verilog_src(env, "bbb\\aaa.v", include_sv=False) + assert is_verilog_src(env, "aaatb.v", include_sv=False) + assert is_verilog_src(env, "aaa_tb.v", include_sv=False) + assert not is_verilog_src(env, "aaa.sv", include_sv=False) + assert not is_verilog_src(env, "bbb\\aaa.sv", include_sv=False) + assert not is_verilog_src(env, "aaa_tb.sv", include_sv=False) + + # -- Non verilog source names, system-verilog included. assert not is_verilog_src(env, "aaatb.vv") assert not is_verilog_src(env, "aaatb.V") assert not is_verilog_src(env, "aaa_tb.vh") + # -- Non verilog source names, system-verilog excluded. + assert not is_verilog_src(env, "aaatb.vv", include_sv=False) + assert not is_verilog_src(env, "aaatb.V", include_sv=False) + assert not is_verilog_src(env, "aaa_tb.vh", include_sv=False) + def test_env_args(): """Tests the scons env args retrieval."""