diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..5b3fcbcb --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,18 @@ +{ + "recommendations": [ + "streetsidesoftware.code-spell-checker", + "ms-python.black-formatter", + "alefragnani.bookmarks", + "pbkit.vscode-pbkit", + "ms-python.vscode-pylance", + "ms-python.python", + "ms-python.debugpy", + "kukdh1.verible-formatter", + "mshr-h.veriloghdl", + "shakram02.bash-beautify", + "grapecity.gc-excelviewer", + "ybaumes.highlight-trailing-white-spaces", + "yzhang.markdown-all-in-one", + "shd101wyy.markdown-preview-enhanced" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 5239bd17..49857ca2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,8 +7,8 @@ "request": "launch", "program": "${workspaceFolder}/apio/main.py", "args": [ - "system", - "info" + "raw", + "which" ], "console": "integratedTerminal", "justMyCode": false, diff --git a/.vscode/settings.json b/.vscode/settings.json index 3b39dccb..ffc3183b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,128 @@ ], "black-formatter.args": [ "--line-length=79" + ], + "cSpell.ignorePaths": [ + "apio/common/proto/apio_pb2.py", + "apio/common/proto/apio_pb2.pyi" + ], + "cSpell.words": [ + "aaatb", + "addfinalizer", + "addoption", + "addrmap", + "alredy", + "apio", + "archs", + "argname", + "atools", + "badcommand", + "Basenames", + "bitstream", + "blinky", + "boardmeta", + "bootmeta", + "buildable", + "bver", + "capsys", + "cenv", + "cerror", + "colorlight", + "COMBDLY", + "cout", + "cprint", + "cstyle", + "cunstyle", + "cwarning", + "cwidth", + "dblite", + "Deconflicting", + "determinisitc", + "devides", + "devmgmt", + "dialout", + "dumpfile", + "envlist", + "EXMPLS", + "FGPA", + "fpgas", + "ftdi", + "FTDIUSB", + "GENCODE", + "González", + "gowin", + "htmlcov", + "hver", + "hwid", + "Hymel", + "Hymel's", + "icepll", + "iceprog", + "ICESTORM", + "Icestudio", + "issuecomment", + "itercontent", + "iverilog", + "jasonc", + "Jesús", + "Kbytes", + "kextload", + "kextstat", + "kextunload", + "Kravets", + "ledon", + "libftdi", + "libusb", + "lsftdi", + "lsserial", + "lsusb", + "mabc", + "mengbo", + "nameof", + "nextpnr", + "nofailsafe", + "nostyle", + "nowarn", + "obijuan", + "outcallback", + "pinout", + "posargs", + "prevalidated", + "prój", + "protoc", + "pwwang", + "pylint", + "Pypi", + "pyserial", + "pytest", + "pyxx", + "readmemh", + "sayno", + "sayyes", + "scons", + "sconsign", + "sconstruct", + "Shwan", + "sipeed", + "srcs", + "Sucess", + "testbench", + "testbenches", + "testpypi", + "tinyprog", + "truecolor", + "udevadm", + "ujprog", + "unstyling", + "USBFTDI", + "userimage", + "usermod", + "venv", + "Veriable", + "verible", + "verilator", + "verilog", + "yosys", + "Zadig" ] -} \ No newline at end of file +} diff --git a/COMMANDS.md b/COMMANDS.md index c5dc1109..0ec476f1 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -4,6 +4,9 @@ * [apio build](#apio-build) - Synthesize the bitstream. * [apio clean](#apio-clean) - Delete the apio generated files. * [apio create](#apio-create) - Create an apio.ini project file. + * [apio docs](#apio-docs) - Read apio documentations. + * [apio docs options](#apio-docs-options) - Apio.ini options documentation. + * [apio docs resources](#apio-docs-resources) - Information about online resources. * [apio drivers](#apio-drivers) - Manage the operating system drivers. * [apio drivers install](#apio-drivers-install) - Install drivers. * [apio drivers install ftdi](#apio-drivers-install-ftdi) - Install the ftdi drivers. @@ -51,25 +54,27 @@ Usage: apio [OPTIONS] COMMAND [ARGS]... Work with FPGAs with ease. Apio is an easy to use and open-source command-line suite designed to - streamline FPGA programming. It supports a wide range of tasks, including - linting, building, simulation, unit testing, and programming FPGA boards. + streamline FPGA programming. It supports a wide range of tasks, + including linting, building, simulation, unit testing, and programming + FPGA boards. - An Apio project consists of a directory containing a configuration file - named 'apio.ini', along with FPGA source files, testbenches, and pin - definition files. + An Apio project consists of a directory containing a configuration + file named 'apio.ini', along with FPGA source files, testbenches, and + pin definition files. - Apio commands are intuitive and perform their intended functionalities right - out of the box. For example, the command apio upload automatically compiles - the design in the current directory and uploads it to the FPGA board. + Apio commands are intuitive and perform their intended functionalities + right out of the box. For example, the command apio upload + automatically compiles the design in the current directory and uploads + it to the FPGA board. - For detailed information about any Apio command, append the -h flag to view - its help text. For instance: + For detailed information about any Apio command, append the -h flag to + view its help text. For example: apio build -h apio drivers ftdi install -h - For more information about the Apio project, visit the official Apio Wiki - https://github.com/FPGAwars/apio/wiki/Apio + For more information about the Apio project, visit the official Apio + Wiki https://github.com/FPGAwars/apio/wiki/Apio Options: --version Show the version and exit. @@ -95,6 +100,7 @@ Setup commands: apio drivers Manage the operating system drivers. Utility commands: + apio docs Read apio documentations. apio boards List available board definitions. apio fpgas List available FPGA definitions. apio examples List and fetch apio examples. @@ -111,9 +117,10 @@ Utility commands: ``` Usage: apio boards [OPTIONS] - The command 'apio boards' lists the FPGA boards recognized by Apio. Custom - boards can be defined by placing a custom 'boards.jsonc' file in the project - directory, which will override Apio’s default 'boards.jsonc' file. + The command 'apio boards' lists the FPGA boards recognized by Apio. + Custom boards can be defined by placing a custom 'boards.jsonc' file + in the project directory, which will override Apio’s default + 'boards.jsonc' file. Examples: apio boards # List all boards. @@ -124,6 +131,7 @@ Options: -v, --verbose Show detailed output. -p, --project-dir path Set the root directory for the project. -h, --help Show this message and exit. + ```
@@ -133,12 +141,12 @@ Options: ``` Usage: apio build [OPTIONS] - The command 'apio build' processes the project’s source files and generates - a bitstream file, which can then be uploaded to your FPGA. + The command 'apio build' processes the project’s source files and + generates a bitstream file, which can then be uploaded to your FPGA. - The 'apio build' command compiles all .v files (e.g., my_module.v) in the - project directory, except those whose names end with _tb (e.g., - my_module_tb.v), as these are assumed to be testbenches. + The 'apio build' command compiles all .v files (e.g., my_module.v) in + the project directory, except those whose names end with '_tb' (e.g., + my_module_tb.v) which are assumed to be testbenches. Examples: apio build # Build @@ -150,6 +158,7 @@ Options: --verbose-synth Show detailed synth stage output. --verbose-pnr Show detailed pnr stage output. -h, --help Show this message and exit. + ```
@@ -159,8 +168,8 @@ Options: ``` Usage: apio clean [OPTIONS] - The command 'apio clean' removes temporary files generated in the project - directory by previous Apio commands. + The command 'apio clean' removes temporary files generated in the + project directory by previous Apio commands. Example: apio clean @@ -168,6 +177,7 @@ Usage: apio clean [OPTIONS] Options: -p, --project-dir path Set the root directory for the project. -h, --help Show this message and exit. + ```
@@ -177,7 +187,7 @@ Options: ``` Usage: apio create [OPTIONS] - The command 'apio create' creates a new `apio.ini` project file and is + The command 'apio create' creates a new 'apio.ini' project file and is typically used when setting up a new Apio project. Examples: @@ -185,14 +195,71 @@ Usage: apio create [OPTIONS] apio create --board alhambra-ii --top-module MyModule [Note] This command only creates a new 'apio.ini' file, rather than a - complete and buildable project. To create complete projects, refer to the - 'apio examples' command. + complete and buildable project. To create complete projects, refer to + the 'apio examples' command. Options: -b, --board BOARD Set the board. [required] -t, --top-module name Set the top level module name. -p, --project-dir path Set the root directory for the project. -h, --help Show this message and exit. + +``` + +
+ +### apio docs + +``` +Usage: apio docs [OPTIONS] COMMAND [ARGS]... + + The command group 'apio docs' contains subcommands that provides + various apio documentation and references to online resources. + +Options: + -h, --help Show this message and exit. + +Subcommands: + apio docs options Apio.ini options documentation. + apio docs resources Information about online resources. + +``` + +
+ +### apio docs options + +``` +Usage: apio docs options [OPTIONS] [OPTION] + + The command 'apio docs options' provides information about the + required project file 'apio.ini'. + + Examples: + apio docs options # List an overview and all options. + apio docs options top-module # List a single option. + +Options: + -h, --help Show this message and exit. + +``` + +
+ +### apio docs resources + +``` +Usage: apio docs resources [OPTIONS] + + The command 'apio docs resources' provides information about apio + related online resources. + + Examples: + apio docs resources # Provides resources information + +Options: + -h, --help Show this message and exit. + ```
@@ -202,8 +269,8 @@ Options: ``` Usage: apio drivers [OPTIONS] COMMAND [ARGS]... - The command group ‘apio drivers’ contains subcommands to manage the drivers - on your system. + The command group 'apio drivers' contains subcommands to manage the + drivers on your system. Options: -h, --help Show this message and exit. @@ -222,8 +289,9 @@ Subcommands: ``` Usage: apio drivers install [OPTIONS] COMMAND [ARGS]... - The command group 'apio drivers install' includes subcommands that that - install system drivers that are used to upload designs to FPGA boards. + The command group 'apio drivers install' includes subcommands that + that install system drivers that are used to upload designs to FPGA + boards. Options: -h, --help Show this message and exit. @@ -241,14 +309,15 @@ Subcommands: ``` Usage: apio drivers install ftdi [OPTIONS] - The command 'apio drivers install ftdi' installs on your system the FTDI - drivers required by some FPGA boards. + The command 'apio drivers install ftdi' installs on your system the + FTDI drivers required by some FPGA boards. Examples: - apio drivers install ftdi # Install the ftdi drivers. + apio drivers install ftdi # Install the ftdi drivers. Options: -h, --help Show this message and exit. + ```
@@ -258,14 +327,15 @@ Options: ``` Usage: apio drivers install serial [OPTIONS] - The command ‘apio drivers install serial’ installs the necessary serial - drivers on your system, as required by certain FPGA boards. + The command 'apio drivers install serial' installs the necessary + serial drivers on your system, as required by certain FPGA boards. Examples: - apio drivers install serial # Install the serial drivers. + apio drivers install serial # Install the serial drivers. Options: -h, --help Show this message and exit. + ```
@@ -275,8 +345,8 @@ Options: ``` Usage: apio drivers list [OPTIONS] COMMAND [ARGS]... - The command group 'apio drivers list' includes subcommands that that lists - system drivers that are used with FPGA boards. + The command group 'apio drivers list' includes subcommands that that + lists system drivers that are used with FPGA boards. Options: -h, --help Show this message and exit. @@ -295,18 +365,19 @@ Subcommands: ``` Usage: apio drivers list ftdi [OPTIONS] - The command 'apio drivers list ftdi' displays the FTDI devices currently - connected to your computer. It is useful for diagnosing FPGA board - connectivity issues. + The command 'apio drivers list ftdi' displays the FTDI devices + currently connected to your computer. It is useful for diagnosing FPGA + board connectivity issues. Examples: - apio drivers list ftdi # List the ftdi devices. + apio drivers list ftdi # List the ftdi devices. [Hint] This command uses the lsftdi utility, which can also be invoked - directly with the 'apio raw -- lsftdi ' command. + directly with the 'apio raw -- lsftdi ...' command. Options: -h, --help Show this message and exit. + ```
@@ -316,17 +387,19 @@ Options: ``` Usage: apio drivers list serial [OPTIONS] - The command ‘apio drivers list serial’ lists the serial devices connected to - your computer. It is useful for diagnosing FPGA board connectivity issues. + The command 'apio drivers list serial' lists the serial devices + connected to your computer. It is useful for diagnosing FPGA board + connectivity issues. Examples: - apio drivers list serial # List the serial devices. + apio drivers list serial # List the serial devices. - [Hint] This command executes the utility lsserial, which can also be invoked - using the command 'apio raw -- lsserial '. + [Hint] This command executes the utility lsserial, which can also be + invoked using the command 'apio raw -- lsserial ...'. Options: -h, --help Show this message and exit. + ```
@@ -336,18 +409,19 @@ Options: ``` Usage: apio drivers list usb [OPTIONS] - The command ‘apio drivers list usb runs the lsusb utility to list the USB - devices connected to your computer. It is typically used for diagnosing - connectivity issues with FPGA boards. + The command 'apio drivers list usb' runs the lsusb utility to list the + USB devices connected to your computer. It is typically used for + diagnosing connectivity issues with FPGA boards. Examples: - apio drivers list usb # List the usb devices + apio drivers list usb # List the usb devices - [Hint] You can also run the lsusb utility using the command 'apio raw -- - lsusb '. + [Hint] You can also run the lsusb utility using the command 'apio raw + -- lsusb ...'. Options: -h, --help Show this message and exit. + ```
@@ -357,8 +431,9 @@ Options: ``` Usage: apio drivers uninstall [OPTIONS] COMMAND [ARGS]... - The command group 'apio drivers uninstall' includes subcommands that that - uninstall system drivers that are used to upload designs to FPGA boards. + The command group 'apio drivers uninstall' includes subcommands that + that uninstall system drivers that are used to upload designs to FPGA + boards. Options: -h, --help Show this message and exit. @@ -376,14 +451,15 @@ Subcommands: ``` Usage: apio drivers uninstall ftdi [OPTIONS] - The command 'apio drivers uninstall ftdi' removes the FTDI drivers that may - have been installed earlier. + The command 'apio drivers uninstall ftdi' removes the FTDI drivers + that may have been installed earlier. Examples: - apio drivers uninstall ftdi # Uninstall the ftdi drivers. + apio drivers uninstall ftdi # Uninstall the ftdi drivers. Options: -h, --help Show this message and exit. + ```
@@ -393,14 +469,15 @@ Options: ``` Usage: apio drivers uninstall serial [OPTIONS] - The command ‘apio drivers uninstall serial’ removes the serial drivers that - you may have installed earlier. + The command 'apio drivers uninstall serial' removes the serial drivers + that you may have installed earlier. Examples: - apio drivers uinstall serial # Uinstall the serial drivers. + apio drivers uninstall serial # Uninstall the serial drivers. Options: -h, --help Show this message and exit. + ```
@@ -410,9 +487,9 @@ Options: ``` Usage: apio examples [OPTIONS] COMMAND [ARGS]... - The command group ‘apio examples’ provides subcommands for listing and - fetching Apio-provided examples. Each example is a self-contained mini- - project that can be built and uploaded to an FPGA board. + The command group 'apio examples' provides subcommands for listing and + fetching Apio-provided examples. Each example is a self-contained + mini-project that can be built and uploaded to an FPGA board. Options: -h, --help Show this message and exit. @@ -431,20 +508,19 @@ Subcommands: ``` Usage: apio examples fetch [OPTIONS] EXAMPLE - The command ‘apio examples fetch’ fetches the files of the specified example - to the current directory or to the directory specified by the –dst option. - The destination directory does not need to exist, but if it does, it must be - empty. + The command 'apio examples fetch' fetches the files of the specified + example to the current directory or to the directory specified by the + '-dst' option. The destination directory does not need to exist, but + if it does, it must be empty. Examples: apio examples fetch alhambra-ii/ledon apio examples fetch alhambra-ii/ledon -d foo/bar - [Hint] For the list of available examples, type ‘apio examples list’. - Options: -d, --dst path Set a different destination directory. -h, --help Show this message and exit. + ```
@@ -454,20 +530,18 @@ Options: ``` Usage: apio examples fetch-board [OPTIONS] BOARD - The command ‘apio examples fetch-board’ is used to fetch all the Apio + The command 'apio examples fetch-board' is used to fetch all the Apio examples for a specific board. The examples are copied to the current - directory or to the specified destination directory if the –dst option is - provided. + directory or to the specified destination directory if the '–-dst' + option is provided. Examples: - apio examples fetch-board alhambra-ii # Fetch to local directory - apio examples fetch-board alhambra-ii -d foo/bar # Fetch to foo/bar - - [Hint] For the list of available examples, type ‘apio examples list’. + apio examples fetch-board alhambra-ii # Fetch board examples. Options: -d, --dst path Set a different destination directory. -h, --help Show this message and exit. + ```
@@ -477,20 +551,19 @@ Options: ``` Usage: apio examples list [OPTIONS] - The command ‘apio examples list’ lists the available Apio project examples - that you can use. + The command 'apio examples list' lists the available Apio project + examples that you can use. Examples: apio examples list # List all examples - apio examples list -v # List with extra information. - apio examples list | grep alhambra-ii # Show examples of a specific board. - apio examples list | grep -i blink # Show all blinking examples. - - + apio examples list -v # More verbose output. + apio examples list | grep alhambra-ii # Show alhambra-ii examples. + apio examples list | grep -i blink # Show blinking examples. Options: -v, --verbose Show detailed output. -h, --help Show this message and exit. + ```
@@ -500,38 +573,41 @@ Options: ``` Usage: apio format [OPTIONS] [FILES]... - The command ‘apio format’ formats Verilog source files to ensure consistency - and style without altering their semantics. The command accepts the names of - pecific source files to format or formats all project source files by - default. + The command 'apio format' formats Verilog source files to ensure + consistency and style without altering their semantics. The command + accepts the names of specific source files to format or formats all + project source files by default. Examples: apio format # Format all source files. - apio format -v # Same as above but with verbose output. - apio format main.v main_tb.v # Format the two tiven files. + apio format -v # Same but with verbose output. + apio format main.v main_tb.v # Format the two files. + + The format command utilizes the format tool from the Verible project, + which can be configured by setting its flags in the apio.ini project + file For example: - The format command utilizes the format tool from the Verible project, which - can be configured by setting its flags in the apio.ini project file For - example: format-verible-options = --column_limit=80 --indentation_spaces=4 - If needed, sections of source code can be protected from formatting using - Verible formatter directives: + If needed, sections of source code can be protected from formatting + using Verible formatter directives: // verilog_format: off ... untouched code ... // verilog_format: on - For a full list of Verible formatter flags, refer to the documentation page - online or use the command 'apio raw -- verible-verilog-format --helpful'. + For a full list of Verible formatter flags, refer to the documentation + page online or use the command 'apio raw -- verible-verilog-format + --helpful'. Options: -p, --project-dir path Set the root directory for the project. -v, --verbose Show detailed output. -h, --help Show this message and exit. + ```
@@ -541,10 +617,10 @@ Options: ``` Usage: apio fpgas [OPTIONS] - The command ‘apio fpgas’ lists the FPGAs recognized by Apio. Custom FPGAs - supported by the underlying Yosys toolchain can be defined by placing a - custom fpgas.jsonc file in the project directory, overriding Apio’s standard - fpgas.jsonc file. + The command 'apio fpgas' lists the FPGAs recognized by Apio. Custom + FPGAs supported by the underlying Yosys toolchain can be defined by + placing a custom 'fpgas.jsonc' file in the project directory, + overriding Apio’s standard 'fpgas.jsonc' file. Examples: apio fpgas # List all fpgas. @@ -555,6 +631,7 @@ Options: -v, --verbose Show detailed output. -p, --project-dir path Set the root directory for the project. -h, --help Show this message and exit. + ```
@@ -564,8 +641,8 @@ Options: ``` Usage: apio graph [OPTIONS] - The command ‘apio graph’ generates a graphical representation of the Verilog - code in the project. + The command 'apio graph' generates a graphical representation of the + Verilog code in the project. Examples: apio graph # Generate a svg file. @@ -574,8 +651,9 @@ Usage: apio graph [OPTIONS] apio graph --png # Generate a png file. apio graph -t my_module # Graph my_module module. - [Hint] On Windows, type ‘explorer _build/hardware.svg’ to view the graph, - and on Mac OS type ‘open _build/hardware.svg’. + + [Hint] On Windows, type 'explorer _build/hardware.svg' to view the + graph, and on Mac OS type 'open _build/hardware.svg'. Options: --svg Generate a svg file (default). @@ -585,6 +663,7 @@ Options: -t, --top-module name Set the name of the top module to graph. -v, --verbose Show detailed output. -h, --help Show this message and exit. + ```
@@ -594,9 +673,9 @@ Options: ``` Usage: apio lint [OPTIONS] - The command ‘apio lint’ scans the project’s Verilog code and reports errors, - inconsistencies, and style violations. The command uses the Verilator tool, - which is included in the standard Apio installation. + The command 'apio lint' scans the project's Verilog code and reports + errors, inconsistencies, and style violations. The command uses the + Verilator tool, which is included in the standard Apio installation. Examples: apio lint @@ -604,13 +683,15 @@ Usage: apio lint [OPTIONS] apio lint --all Options: - -t, --top-module name Restrict linting to this module and its depedencies. - -a, --all Enable all warnings, including code style warnings. --nostyle Disable all style warnings. --nowarn nowarn Disable specific warning(s). --warn warn Enable specific warning(s). + -a, --all Enable all warnings, including code style warnings. + -t, --top-module name Restrict linting to this module and its + dependencies. -p, --project-dir path Set the root directory for the project. -h, --help Show this message and exit. + ```
@@ -620,14 +701,13 @@ Options: ``` Usage: apio packages [OPTIONS] COMMAND [ARGS]... - The command group ‘apio packages’ provides commands to manage the - installation of Apio packages. These are not Python packages but Apio- - specific packages containing various tools and data essential for the - operation of Apio. These packages are installed after the installation of - the Apio Python package itself, using the command ‘apio packages install’. + The command group 'apio packages' provides commands to manage the + installation of Apio packages. These are not Python packages but + Apio-specific packages containing various tools and data essential for + the operation of Apio. - The list of available packages depends on the operating system you are using - and may vary between different operating systems. + The list of available packages depends on the operating system you are + using and may vary between different operating systems. Options: -h, --help Show this message and exit. @@ -647,14 +727,15 @@ Subcommands: ``` Usage: apio packages fix [OPTIONS] - The command ‘apio packages fix’ removes broken or obsolete packages that are - listed as broken by the command ‘apio packages list’. + The command 'apio packages fix' removes broken or obsolete packages + that are listed as broken by the command 'apio packages list'. Examples: apio packages fix # Fix package errors, if any. Options: -h, --help Show this message and exit. + ```
@@ -664,22 +745,25 @@ Options: ``` Usage: apio packages install [OPTIONS] PACKAGE - The command ‘apio packages install’ installs Apio packages that are required - for the operation of Apio on your system. + The command 'apio packages install' installs Apio packages that are + required for the operation of Apio on your system. Examples: - apio packages install # Install all missing packages. - apio packages install --force # Re/install all missing packages. - apio packages install oss-cad-suite # Install just this package. - apio packages install examples@0.0.32 # Install a specific version. + apio packages install # Install missing packages. + apio packages install --force # Reinstall all packages. + apio packages install oss-cad-suite # Install package. + apio packages install examples@0.0.32 # Install a specific + version. - Adding the --force option forces the reinstallation of existing packages; - otherwise, packages that are already installed correctly remain unchanged. + Adding the '--force' option forces the reinstallation of existing + packages; otherwise, packages that are already installed correctly + remain unchanged. Options: -f, --force Force installation. -v, --verbose Show detailed output. -h, --help Show this message and exit. + ```
@@ -689,15 +773,16 @@ Options: ``` Usage: apio packages list [OPTIONS] - The command ‘apio packages list’ lists the available and installed Apio - packages. The list of available packages depends on the operating system you - are using and may vary between operating systems. + The command 'apio packages list' lists the available and installed + Apio packages. The list of available packages depends on the operating + system you are using and may vary between operating systems. Examples: apio packages list Options: -h, --help Show this message and exit. + ```
@@ -707,17 +792,18 @@ Options: ``` Usage: apio packages uninstall [OPTIONS] PACKAGE - The command ‘apio packages uninstall’ removes installed Apio packages from - your system. The command does not uninstall the Apio tool itself. + The command 'apio packages uninstall' removes installed Apio packages + from your system. The command does not uninstall the Apio tool itself. Examples: - apio packages uninstall # Uninstall all packages - apio packages uninstall oss-cad-suite # Uninstall a package - apio packages uninstall oss-cad-suite examples # Uninstall two packages + apio packages uninstall # Uninstall all packages + apio packages uninstall oss-cad-suite # Uninstall a package + apio packages uninstall verible examples # Uninstall two packages Options: -v, --verbose Show detailed output. -h, --help Show this message and exit. + ```
@@ -727,12 +813,13 @@ Options: ``` Usage: apio preferences [OPTIONS] COMMAND [ARGS]... - The command group ‘apio preferences' contains subcommands to manage the apio - user preferences. These are user configurations that affect all the apio - project on the same computer. + The command group 'apio preferences' contains subcommands to manage + the apio user preferences. These are user configurations that affect + all the apio projects that use the same apio home directory (e.g. + '~/.apio'). - The user preference is not part of any apio project and typically are not - shared when multiple user colaborate on the same project. + The user preference is not part of any apio project and typically are + not shared when multiple user collaborate on the same project. Options: -h, --help Show this message and exit. @@ -750,15 +837,15 @@ Subcommands: ``` Usage: apio preferences list [OPTIONS] - The command ‘apio preferences list’ lists the current user preferences. + The command 'apio preferences list' lists the current user + preferences. Examples: - apio preferences list # List the user preferences. - - + apio preferences list # List the user preferences. Options: -h, --help Show this message and exit. + ```
@@ -768,12 +855,12 @@ Options: ``` Usage: apio preferences set [OPTIONS] - The command ‘apio preferences set' allows to set the supported user + The command 'apio preferences set' allows to set the supported user preferences. Examples: - apio preferences set --colors yes # Select multi-color output. - apio preferences set --colors no # Select monochrome output. + apio preferences set --colors on # Enable colors. + apio preferences set --colors off # Disable colors. The apio colors are optimized for a terminal windows with a white background. @@ -781,6 +868,7 @@ Usage: apio preferences set [OPTIONS] Options: -c, --colors [on|off] Set/reset colors mode. [required] -h, --help Show this message and exit. + ```
@@ -790,30 +878,31 @@ Options: ``` Usage: apio raw [OPTIONS] COMMAND - The command ‘apio raw’ allows you to bypass Apio and run underlying tools - directly. This is an advanced command that requires familiarity with the - underlying tools. + The command 'apio raw' allows you to bypass Apio and run underlying + tools directly. This is an advanced command that requires familiarity + with the underlying tools. - Before running the command, Apio temporarily modifies system environment - variables such as $PATH to provide access to its packages. To view these - environment changes, run the command with the -v option. + Before running the command, Apio temporarily modifies system + environment variables such as '$PATH' to provide access to its + packages. To view these environment changes, run the command with the + '-v' option. Examples: - apio raw -- yosys --version # Yosys version - apio raw -v -- yosys --version # Same but with verbose apio info. - apio raw -- yosys # Run Yosys in interactive mode. - apio raw -- icepll -i 12 -o 30 # Calc ICE PLL - apio raw -v # Show apio env setting. - apio raw -h # Show this help info. + apio raw -- yosys --version # Yosys version + apio raw -v -- yosys --version # Verbose apio info. + apio raw -- yosys # Yosys interactive mode. + apio raw -- icepll -i 12 -o 30 # Calc ICE PLL. + apio raw -- which yosys # Lookup a command. + apio raw -v # Show apio env setting. + apio raw -h # Show this help info. - The -- token is used to separate Apio commands and their arguments from the - underlying tools and their arguments. It can be omitted in some cases, but - it’s a good practice to always use it. As a rule of thumb, always prefix the - raw command you want to run with 'apio raw -- '. + The '--' marker is used to separate between the arguments of the apio + command itself and those of the executed command. Options: -v, --verbose Show detailed output. -h, --help Show this message and exit. + ```
@@ -823,18 +912,20 @@ Options: ``` Usage: apio report [OPTIONS] - The command ‘apio report’ provides information on the utilization and timing - of the design. It is useful for analyzing utilization bottlenecks and - verifying that the design can operate at the desired clock speed. + The command 'apio report' provides information on the utilization and + timing of the design. It is useful for analyzing utilization + bottlenecks and verifying that the design can operate at the desired + clock speed. Examples: - apio report - epio report --verbose + apio report # Print report. + apio report --verbose # Print extra information. Options: -p, --project-dir path Set the root directory for the project. -v, --verbose Show detailed output. -h, --help Show this message and exit. + ```
@@ -844,37 +935,39 @@ Options: ``` Usage: apio sim [OPTIONS] [TESTBENCH] - The command ‘apio sim’ simulates the default or the specified testbench file - and displays its simulation results in a graphical GTKWave window. The - testbench is expected to have a name ending with _tb, such as main_tb.v or - main_tb.sv. The default testbench file can be specified using the apio.ini - option ‘default-testbench’. If 'default-testbench' is not specified and the - project has exactly one testbench file, that file will be used as the - default testbench. + The command 'apio sim' simulates the default or the specified + testbench file and displays its simulation results in a graphical + GTKWave window. The testbench is expected to have a name ending with + _tb, such as main_tb.v or main_tb.sv. The default testbench file can + be specified using the apio.ini option 'default-testbench'. If + 'default-testbench' is not specified and the project has exactly one + testbench file, that file will be used as the default testbench. Example: - apio sim # Simulate the default testbench file. - apio sim my_module_tb.v # Simulate the specified testbench file. + apio sim # Simulate the default testbench. + apio sim my_module_tb.v # Simulate the specified testbench. - [Important] Avoid using the Verilog $dumpfile() function in your - testbenches, as this may override the default name and location Apio sets - for the generated .vcd file. + [Important] Avoid using the Verilog '$dumpfile()' function in your + testbenches, as this may override the default name and location Apio + sets for the generated .vcd file. - The sim command defines the INTERACTIVE_SIM macro, which can be used in the - testbench to distinguish between ‘apio test’ and ‘apio sim’. For example, - you can use this macro to ignore certain errors when running with ‘apio sim’ - and view the erroneous signals in GTKWave. + The sim command defines the INTERACTIVE_SIM macro, which can be used + in the testbench to distinguish between 'apio test' and 'apio sim'. + For example, you can use this macro to ignore certain errors when + running with 'apio sim' and view the erroneous signals in GTKWave. For a sample testbench that utilizes this macro, see the example at: - https://github.com/FPGAwars/apio-examples/tree/master/upduino31/testbench + https://github.com/FPGAwars/apio-examples/tree/master/upduino31/testbe + nch - [Hint] When configuring the signals in GTKWave, save the configuration so - you don’t need to repeat it each time you run the simulation. + [Hint] When configuring the signals in GTKWave, save the configuration + so you don’t need to repeat it each time you run the simulation. Options: -f, --force Force simulation. -p, --project-dir path Set the root directory for the project. -h, --help Show this message and exit. + ```
@@ -884,7 +977,7 @@ Options: ``` Usage: apio system [OPTIONS] COMMAND [ARGS]... - The command group ‘apio system’ contains subcommands that provide + The command group 'apio system' contains subcommands that provide information about the system and Apio’s installation. Options: @@ -903,20 +996,21 @@ Subcommands: ``` Usage: apio system info [OPTIONS] - The command ‘apio system info’ provides general information about your + The command 'apio system info' provides general information about your system and Apio installation, which is useful for diagnosing Apio installation issues. Examples: - apio system info # Show platform id and info. + apio system info # Show general info. - [Advanced] The default location of the Apio home directory, where - preferences and packages are stored, is in the .apio directory under the - user’s home directory. This location can be changed using the APIO_HOME_DIR - environment variable. + [Advanced] The default location of the Apio home directory, where apio + saves preferences and packages, is in the '.apio' directory under the + user home directory but can be changed using the system environment + variable 'APIO_HOME_DIR'. Options: -h, --help Show this message and exit. + ```
@@ -947,28 +1041,31 @@ Options: ``` Usage: apio test [OPTIONS] [TESTBENCH_FILE] - The command ‘apio test’ simulates one or all the testbenches in the project - and is useful for automated testing of your design. Testbenches are expected - to have names ending with _tb (e.g., my_module_tb.v) and should exit with - the $fatal directive if an error is detected. + The command 'apio test' simulates one or all the testbenches in the + project and is useful for automated testing of your design. + Testbenches are expected to have names ending with _tb (e.g., + my_module_tb.v) and should exit with the '$fatal' directive if an + error is detected. - Examples + Examples: apio test # Run all *_tb.v testbenches. - apio test my_module_tb.v # Run a single testbench + apio test my_module_tb.v # Run a single testbench. - [Important] Avoid using the Verilog $dumpfile() function in your - testbenches, as this may override the default name and location Apio sets - for the generated .vcd file. + [Important] Avoid using the Verilog '$dumpfile()' function in your + testbenches, as this may override the default name and location Apio + sets for the generated .vcd file. For a sample testbench compatible with Apio features, see: - https://github.com/FPGAwars/apio-examples/tree/master/upduino31/testbench + https://github.com/FPGAwars/apio-examples/tree/master/upduino31/testbe + nch [Hint] To simulate a testbench with a graphical visualization of the - signals, refer to the ‘apio sim’ command. + signals, refer to the 'apio sim' command. Options: -p, --project-dir path Set the root directory for the project. -h, --help Show this message and exit. + ```
@@ -978,14 +1075,15 @@ Options: ``` Usage: apio upgrade [OPTIONS] - The command ‘apio upgrade’ checks for the version of the latest Apio release - and provides upgrade directions if necessary. + The command 'apio upgrade' checks for the version of the latest Apio + release and provides upgrade directions if necessary. Examples: apio upgrade Options: -h, --help Show this message and exit. + ```
@@ -995,8 +1093,8 @@ Options: ``` Usage: apio upload [OPTIONS] - The command ‘apio upload’ builds the bitstream file (similar to the apio - build command) and uploads it to the FPGA board. + The command 'apio upload' builds the bitstream file (similar to the + 'apio build' command) and uploads it to the FPGA board. Examples: apio upload @@ -1008,4 +1106,5 @@ Options: -f, --flash Perform FLASH programming. -p, --project-dir path Set the root directory for the project. -h, --help Show this message and exit. + ``` diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 52f5414b..4613bea5 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -16,7 +16,7 @@ For complete tests with several python versions run the command below. make check-all ``` -For quick tests that that don't load lengthy packagtes from the internet +For quick tests that that don't load lengthy packages from the internet run the command below. It will skip all the tests that require internet connection. @@ -53,7 +53,7 @@ pytest test/code_commands/test_build.py ## Using APIO_DEBUG to print debug information -To print internal debugging informaion define the environment variable ``APIO_DEBUG`` before running the apio command. The value of ``APIO_DEBUG`` doesn't matter as long as it's defined. Currently the the debugging information is mostly for commands that invoke scons. +To print internal debugging information define the environment variable ``APIO_DEBUG`` before running the apio command. The value of ``APIO_DEBUG`` doesn't matter as long as it's defined. Currently the the debugging information is mostly for commands that invoke scons. Linux and Mac OSX: ``` @@ -89,7 +89,7 @@ The debug target can be viewed here https://github.com/FPGAwars/apio/blob/develo ## Debugging SConstruct scripts (subprocesses) with Visual Studio Code. To debug the scons scripts, which are run as apio subprocesses, we use a different method or remote debugging. -To activate, define the sytem env var ``APIO_SCONS_DEBUGGER`` (the value doesn't matter), run apio from the command line, and once it reports that it waits for a debugger, run the VCS ``Attach remote`` debug target to connect to the SConstruct process. +To activate, define the system env var ``APIO_SCONS_DEBUGGER`` (the value doesn't matter), run apio from the command line, and once it reports that it waits for a debugger, run the VCS ``Attach remote`` debug target to connect to the SConstruct process. ## Using the dev repository for apio commands. @@ -119,7 +119,7 @@ Package: /Users/user/projects/apio_dev/repo/apio ### Manage python environment with Conda This section is a tip if you don't have python installed or you want to have independent versions of python isolated by apps or environments. -Conda is a powerful tool for this, and it is multiplatform, providing you a way to work in all operating systems in the same way. +Conda is a powerful tool for this, and it is multi-platform, providing you a way to work in all operating systems in the same way. To install Conda: diff --git a/Makefile b/Makefile index 2b885a86..952c9c23 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # NOTE: Some targets have a shortcuts or alias names that are listed on the same line. -# The are provided for convinience or for backward competibility. For example +# The are provided for convenience or for backward compatibility. For example # 'check-all' has the aliases 'check_all' and 'ca'. # Install dependencies for apio development @@ -54,7 +54,7 @@ lint l: test t: python -m tox --skip-missing-interpreters false -e py313 -- --offline -# Same as 'make test' above but with the oldest supported bypon version. +# Same as 'make test' above but with the oldest supported python version. # # Usage: # make test-oldest @@ -66,8 +66,8 @@ test-oldest to: # Tests and lint, single python version, all tests including online.. -# This is a thorough but slow test and sufficient for testign before -# commiting changes run this before submitting code. +# This is a thorough but slow test and sufficient for testing before +# committing changes run this before submitting code. # # Usage: # make check @@ -78,7 +78,7 @@ check c: python -m tox --skip-missing-interpreters false -e lint,py313 -# Same as 'make check' above but with the oldest supported bypon version. +# Same as 'make check' above but with the oldest supported python version. # # Usage: # make check-oldest @@ -106,7 +106,7 @@ check-all check_all ca: # # Usage: # make publish-test -# make publish_test // deprecated, to be deleted. +# make publish_test // deprecated, to be deleted. # .PHONY: publish-test publish_test publish-test publish_test: @@ -125,7 +125,7 @@ publish: ## Install the tool locally # # Usage: -# make instll +# make install # .PHONY: install install: diff --git a/README.md b/README.md index 33925e95..c3173c5d 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,22 @@ ![][linux-logo]   ![][macosx-logo]   ![][windows-logo]   ![][ubuntu-logo]   ![][raspbian-logo] -**TL;DR, Apio is an extremly easy to install and use open-source toolbox for programming FPGA boards.** +**TL;DR, Apio is an extremely easy to install and use open-source toolbox for programming FPGA boards.** ## What is Apio? -* Apio is an **extremly easy to use** toolbox for FPGA programming. +* Apio is an **extremely easy to use** toolbox for FPGA programming. * Apio is **easy to install**, no more dealing with 'toolcahins', licenses, scripts, and makefiles. * Apio runs on a wide range of platforms, **Linux, Windows, Mac, and more**. * Apio is **open source and free to use**. -* Apio supports **all aspects of FPGA developement cycles**, including building, simulation, testing, and uploading a design. +* Apio supports **all aspects of FPGA development cycles**, including building, simulation, testing, and uploading a design. * **Apio commands are very simple,** for example, ``apio build`` to build, ``apio test`` to test end ``apio upload`` to upload. -* Apio can be used with **any text editor** and also **playes well with Visual Studio Code and github**. +* Apio can be used with **any text editor** and also **plays well with Visual Studio Code and github**. * Apio supports out of the **more than 80 boards** and **custom boards can be easily added**. * Apio provides out of the box tens of simple **project examples ready to build and upload**. * Apio currently supports the ``ICE40`` and ``ECP5`` FPGA architecture with ``GOWIN`` architecture in the works. -## The Apio phylosophy +## The Apio philosophy Apio makes **extremely easy** the process of working with **FPGAs**. Go from **scratch** to having a **blinky LED** in your FPGA board in minutes! This is because it is was designed for ease of use and uses only **Free/Libre Open Source Software** (FLOSS). Just install it and use it as you want. diff --git a/apio/__init__.py b/apio/__init__.py index c1c6a718..b1a29d42 100644 --- a/apio/__init__.py +++ b/apio/__init__.py @@ -4,7 +4,7 @@ # -- This file is part of the Apio project # -- (C) 2016-2019 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -------------------------------------------- # - Information for the Distribution package diff --git a/apio/apio_context.py b/apio/apio_context.py index 41311b8c..1e6ffb15 100644 --- a/apio/apio_context.py +++ b/apio/apio_context.py @@ -4,7 +4,7 @@ # -- This file is part of the Apio project # -- (C) 2016-2019 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 import sys import json @@ -13,15 +13,13 @@ from collections import OrderedDict from pathlib import Path from typing import Optional, Dict -import click -from click import secho +from apio.common.apio_console import cout, cerror, cwarning from apio.profile import Profile from apio.utils import jsonc, util, env_options from apio.managers.project import ( Project, ProjectResolver, load_project_from_file, - APIO_INI, ) @@ -43,7 +41,7 @@ PACKAGES_JSONC = "packages.jsonc" # ----------------------------------------- -# ---- File: resources/boads.jsonc +# ---- File: resources/boards.jsonc # ----------------------------------------- # -- Information about all the supported boards # -- names, fpga family, programmer, ftdi description, vendor id, product id @@ -85,7 +83,7 @@ class ApioContextScope(Enum): # pylint: disable=too-many-instance-attributes class ApioContext: - """Apio context. Class for accesing apio resources and configurations.""" + """Apio context. Class for accessing apio resources and configurations.""" def __init__( self, @@ -110,9 +108,9 @@ def __init__( # -- that modify its default behavior. defined_env_options = env_options.get_defined() if defined_env_options: - secho( + cout( f"Active env options [{', '.join(defined_env_options)}].", - fg="yellow", + style="yellow", ) # -- Store the scope @@ -133,7 +131,7 @@ def __init__( ) elif scope == ApioContextScope.PROJECT_OPTIONAL: project_dir = util.resolve_project_dir(project_dir_arg) - if (project_dir / APIO_INI).exists(): + if (project_dir / "apio.ini").exists(): self._project_dir = project_dir else: assert ( @@ -150,7 +148,7 @@ def __init__( self.config = self._load_resource(CONFIG_JSONC) # -- Profile information, from ~/.apio/profile.json. We provide it with - # -- the remote config url template from disribution.jsonc such that + # -- the remote config url template from distribution.jsonc such that # -- can it fetch the remote config on demand. self.profile = Profile(self.home_dir, self.config["remote-config"]) @@ -183,7 +181,7 @@ def __init__( PROGRAMMERS_JSONC, allow_custom=True ) - # -- Sort resources for consistency and intunitiveness. + # -- Sort resources for consistency and intuitiveness. # -- # -- We don't sort the all_packages and platform_packages dictionaries # -- because that will affect the order of the env path items. @@ -234,20 +232,18 @@ def lookup_board_name( # -- Fatal error if unknown board. if strict and canonical_name is None: - secho(f"Error: no such board '{board}'", fg="red") - secho( - "Run 'apio boards' for the list of board names.\n" - "Expecting a board name such as 'alhambra-ii'.", - fg="yellow", + cerror(f"No such board '{board}'") + cout( + "Run 'apio boards' for the list of board names.", + style="yellow", ) sys.exit(1) # -- Warning if caller used a legacy board name. if warn and canonical_name and board != canonical_name: - secho( - f"Warning: '{board}' board name was changed. " - f"Please use '{canonical_name}' instead.", - fg="yellow", + cwarning( + f"'{board}' board name was changed. " + f"Please use '{canonical_name}' instead." ) # -- Return the canonical board name. @@ -301,7 +297,7 @@ def _load_resource(self, name: str, allow_custom: bool = False) -> dict: filepath = self._project_dir / name if filepath.exists(): if allow_custom: - secho(f"Loading custom '{name}'.") + cout(f"Loading custom '{name}'.") return self._load_resource_file(filepath) # -- Load the stock resource file from the APIO package. @@ -311,7 +307,7 @@ def _load_resource(self, name: str, allow_custom: bool = False) -> dict: @staticmethod def _load_resource_file(filepath: Path) -> dict: """Load the resources from a given jsonc file path - * OUTPUT: A dictionary with the jsons file data + * OUTPUT: A dictionary with the jsonc file data In case of error it raises an exception and finish """ @@ -323,21 +319,13 @@ def _load_resource_file(filepath: Path) -> dict: data_jsonc = file.read() # -- The jsonc file NOT FOUND! This is an apio system error - # -- It should never ocurr unless there is a bug in the + # -- It should never occur unless there is a bug in the # -- apio system files, or a bug when calling this function # -- passing a wrong file except FileNotFoundError as exc: - # -- Display Main error - secho("Apio System Error! JSONC file not found", fg="red") - - # -- Display the affected file (in a different color) - apio_file_msg = click.style("Apio file: ", fg="yellow") - filename = click.style(f"{filepath}", fg="cyan", bold=True) - secho(f"{apio_file_msg} {filename}") - - # -- Display the specific error message - secho(f"{exc}\n", fg="red") + # -- Display error information + cerror("[Internal] .jsonc file not found", f"{exc}") # -- Abort! sys.exit(1) @@ -350,20 +338,13 @@ def _load_resource_file(filepath: Path) -> dict: resource = json.loads(data_json) # -- Invalid json format! This is an apio system error - # -- It should never ocurr unless some develeper has - # -- made a mistake when changing the json file + # -- It should never occur unless a developer has + # -- made a mistake when changing the jsonc file except json.decoder.JSONDecodeError as exc: # -- Display Main error - secho("Apio System Error! Invalid JSONC file", fg="red") - - # -- Display the affected file (in a different color) - apio_file_msg = click.style("Apio file: ", fg="yellow") - filename = click.style(f"{filepath}", fg="cyan", bold=True) - secho(f"{apio_file_msg} {filename}") - - # -- Display the specific error message - secho(f"{exc}\n", fg="red") + cerror("Invalid .jsonc file", f"{exc}") + cout(f"File: {filepath}", style="yellow") # -- Abort! sys.exit(1) @@ -376,7 +357,7 @@ def _expand_env_template(template: str, package_path: Path) -> str: """Fills a packages env value template as they appear in packages.jsonc. Currently it recognizes only a single place holder '%p' representing the package absolute path. The '%p" can appear only - at the begigning of the template. + at the beginning of the template. E.g. '%p/bin' -> '/users/user/.apio/packages/drivers/bin' @@ -433,12 +414,12 @@ def _resolve_package_envs( def get_package_info(self, package_name: str) -> str: """Returns the information of the package with given name. - The information is a JSON dict originated at packages.jsnon(). + The information is a JSON dict originated at packages.json(). Exits with an error message if the package is not defined. """ package_info = self.platform_packages.get(package_name, None) if package_info is None: - secho(f"Error: unknown package '{package_name}'", fg="red") + cerror(f"Unknown package '{package_name}'") sys.exit(1) return package_info @@ -462,12 +443,11 @@ def _determine_platform_id(platforms: Dict[str, Dict]) -> str: # -- Verify it's valid. This can be a user error if the override # -- is invalid. if platform_id not in platforms.keys(): - secho(f"Error: unknown platform id: [{platform_id}]") - secho( - "\n" - "[Hint]: For the list of supported platforms\n" + cerror(f"Unknown platform id: [{platform_id}]") + cout( + "For the list of supported platforms " "type 'apio system platforms'.", - fg="yellow", + style="yellow", ) sys.exit(1) @@ -552,14 +532,17 @@ def _get_system_platform_id() -> str: # -- Return the full platform return platform_str + @property def is_linux(self) -> bool: """Returns True iff platform_id indicates linux.""" return "linux" in self.platform_id + @property def is_darwin(self) -> bool: """Returns True iff platform_id indicates Mac OSX.""" return "darwin" in self.platform_id + @property def is_windows(self) -> bool: """Returns True iff platform_id indicates windows.""" return "windows" in self.platform_id @@ -568,7 +551,7 @@ def is_windows(self) -> bool: # pylint: disable=too-few-public-methods class _ProjectResolverImpl(ProjectResolver): def __init__(self, apio_context: ApioContext): - """When ApioContext instanciates this object, ApioContext is fully + """When ApioContext instances this object, ApioContext is fully constructed, except for the project field.""" self._apio_context = apio_context diff --git a/apio/commands/apio.py b/apio/commands/apio.py index cf0ec03e..9721a86e 100644 --- a/apio/commands/apio.py +++ b/apio/commands/apio.py @@ -4,7 +4,7 @@ # -- This file is part of the Apio project # -- (C) 2016-2019 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 import click @@ -31,6 +31,7 @@ apio_test, apio_upgrade, apio_upload, + apio_docs, ) @@ -67,6 +68,7 @@ ApioSubgroup( "Utility commands", [ + apio_docs.cli, apio_boards.cli, apio_fpgas.cli, apio_examples.cli, @@ -92,29 +94,29 @@ def context_settings(): # -- Top click command node. # --------------------------- +# -- Text in the markdown format of the python rich library. APIO_HELP = """ -Work with FPGAs with ease. +[b]Work with FPGAs with ease.[/b] -Apio is an easy to use and open-source command-line suite designed to -streamline FPGA programming. It supports a wide range of tasks, including +Apio is an easy to use and open-source command-line suite designed to \ +streamline FPGA programming. It supports a wide range of tasks, including \ linting, building, simulation, unit testing, and programming FPGA boards. -An Apio project consists of a directory containing a configuration file -named 'apio.ini', along with FPGA source files, testbenches, and pin definition -files. +An Apio project consists of a directory containing a configuration file \ +named 'apio.ini', along with FPGA source files, testbenches, and pin \ +definition files. -Apio commands are intuitive and perform their intended functionalities right -out of the box. For example, the command apio upload automatically compiles +Apio commands are intuitive and perform their intended functionalities right \ +out of the box. For example, the command apio upload automatically compiles \ the design in the current directory and uploads it to the FPGA board. -For detailed information about any Apio command, append the -h flag to view -its help text. For instance: +For detailed information about any Apio command, append the -h flag to view \ +its help text. For example: -\b -apio build -h -apio drivers ftdi install -h +[code]apio build -h +apio drivers ftdi install -h[/code] -For more information about the Apio project, visit the official Apio Wiki +For more information about the Apio project, visit the official Apio Wiki \ https://github.com/FPGAwars/apio/wiki/Apio """ diff --git a/apio/commands/apio_boards.py b/apio/commands/apio_boards.py index cf78ad62..3a253d8e 100644 --- a/apio/commands/apio_boards.py +++ b/apio/commands/apio_boards.py @@ -4,7 +4,7 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio boards' command""" import sys @@ -12,9 +12,12 @@ from dataclasses import dataclass from typing import List, Dict import click -from click import secho, style, echo +from rich.table import Table +from rich import box +from apio.common.apio_console import cout, cprint +from apio.common import apio_console from apio.apio_context import ApioContext, ApioContextScope -from apio.utils import util +from apio.utils import util, cmd_util from apio.commands import options from apio.managers.examples import Examples @@ -39,7 +42,7 @@ class Entry: programmer: str def sort_key(self): - """A key for sorting the fpga entries in our prefered order.""" + """A key for sorting the fpga entries in our preferred order.""" return (util.fpga_arch_sort_key(self.fpga_arch), self.board.lower()) @@ -49,9 +52,6 @@ def sort_key(self): def list_boards(apio_ctx: ApioContext, verbose: bool): """Prints all the available board definitions.""" - # -- Get the output info (terminal vs pipe). - output_config = util.get_terminal_config() - # -- Get examples counts by board. This is a sparse dictionary. examples = Examples(apio_ctx) examples_counts: Dict[str, int] = examples.count_examples_by_board() @@ -88,104 +88,91 @@ def list_boards(apio_ctx: ApioContext, verbose: bool): ) ) - # -- Sort boards by our prefered order. + # -- Sort boards by our preferred order. entries.sort(key=lambda x: x.sort_key()) - # -- Compute the columns widths. - - margin = 2 if verbose else 4 - board_len = max(len(x.board) for x in entries) + margin - 2 - examples_count_len = 7 + margin - board_description_len = ( - max(len(x.board_description) for x in entries) + margin + # -- Define the table. + table = Table( + show_header=True, + show_lines=False, + box=box.SQUARE, + border_style="dim", + title_justify="left", + title="Apio Supported Boards", ) - fpga_arch_len = max(len(x.fpga_arch) for x in entries) + margin - fpga_size_len = max(len(x.fpga_size) for x in entries) + margin - fpga_len = max(len(x.fpga) for x in entries) + margin - fpga_part_num_len = max(len(x.fpga_part_num) for x in entries) + margin - fpga_type_len = max(len(x.fpga_type) for x in entries) + margin - fpga_pack_len = max(len(x.fpga_pack) for x in entries) + margin - fpga_speed_len = 5 + margin - programmer_len = max(len(x.programmer) for x in entries) + margin - - # -- Construct the title fields. - parts = [] - parts.append(f"{'BOARD':<{board_len}}") - parts.append(f"{'EXAMPLES':<{examples_count_len}}") - if verbose: - parts.append(f"{'DESCRIPTION':<{board_description_len}}") - parts.append(f"{'ARCH':<{fpga_arch_len}}") - parts.append(f"{'SIZE':<{fpga_size_len}}") + + # -- Add columns. + table.add_column("BOARD-ID", no_wrap=True, style="cyan") + table.add_column("EXMPLS", no_wrap=True) if verbose: - parts.append(f"{'FPGA-ID':<{fpga_len}}") - parts.append(f"{'PART-NUMBER':<{fpga_part_num_len}}") + table.add_column("DESCRIPTION", no_wrap=True, max_width=25) + table.add_column("ARCH", no_wrap=True) + table.add_column("SIZE", no_wrap=True) if verbose: - parts.append(f"{'TYPE':<{fpga_type_len}}") - parts.append(f"{'PACK':<{fpga_pack_len}}") - parts.append(f"{'SPEED':<{fpga_speed_len}}") - parts.append(f"{'PROGRAMMER':<{programmer_len}}") - - # -- Print the title line. - secho("".join(parts), fg="cyan", bold=True) + table.add_column("FPGA-ID", no_wrap=True) + table.add_column("PART-NUMBER", no_wrap=True) + table.add_column("PROGRAMMER", no_wrap=True) - # -- Print all the boards. + # -- Add rows, with separation line between architecture groups. last_arch = None for entry in entries: - # -- If not piping, add architecture groups seperations. - if last_arch != entry.fpga_arch and output_config.terminal_mode: - echo("") - secho(f"{entry.fpga_arch.upper()}", fg="magenta", bold=True) + # -- If switching architecture, add an horizontal separation line. + if last_arch != entry.fpga_arch and apio_console.is_terminal(): + table.add_section() last_arch = entry.fpga_arch - # -- Construct the line fields. - parts = [] - parts.append(style(f"{entry.board:<{board_len}}", fg="cyan")) - parts.append(f"{entry.examples_count:<{examples_count_len}}") + # -- Collect row values. + values = [] + values.append(entry.board) + values.append(str(entry.examples_count)) if verbose: - parts.append(f"{entry.board_description:<{board_description_len}}") - parts.append(f"{entry.fpga_arch:<{fpga_arch_len}}") - parts.append(f"{entry.fpga_size:<{fpga_size_len}}") + values.append(entry.board_description) + values.append(entry.fpga_arch) + values.append(str(entry.fpga_size)) if verbose: - parts.append(f"{entry.fpga:<{fpga_len}}") - parts.append(f"{entry.fpga_part_num:<{fpga_part_num_len}}") - if verbose: - parts.append(f"{entry.fpga_type:<{fpga_type_len}}") - parts.append(f"{entry.fpga_pack:<{fpga_pack_len}}") - parts.append(f"{entry.fpga_speed:<{fpga_speed_len}}") - parts.append(f"{entry.programmer:<{programmer_len}}") + values.append(entry.fpga) + values.append(entry.fpga_part_num) + values.append(entry.programmer) + + # -- Add row. + table.add_row(*values) - # -- Print the line - secho("".join(parts)) + # -- Render the table. + cout() + cprint(table) # -- Show the summary. - if output_config.terminal_mode: - secho(f"Total of {util.plurality(entries, 'board')}") + if apio_console.is_terminal(): + cout(f"Total of {util.plurality(entries, 'board')}") if not verbose: - secho("Run 'apio boards -v' for additional columns.", fg="yellow") + cout( + "Run 'apio boards -v' for additional columns.", style="yellow" + ) -# --------------------------- -# -- COMMAND -# --------------------------- +# ------------- apio boards + # R0801: Similar lines in 2 files # pylint: disable = R0801 + +# -- Text in the markdown format of the python rich library. APIO_BOARDS_HELP = """ -The command 'apio boards' lists the FPGA boards recognized by Apio. -Custom boards can be defined by placing a custom 'boards.jsonc' file in the +The command 'apio boards' lists the FPGA boards recognized by Apio. \ +Custom boards can be defined by placing a custom 'boards.jsonc' file in the \ project directory, which will override Apio’s default 'boards.jsonc' file. -\b -Examples: +Examples:[code] apio boards # List all boards. apio boards -v # List with extra columns.. - apio boards | grep ecp5 # Filter boards results. + apio boards | grep ecp5 # Filter boards results.[/code] """ @click.command( name="boards", + cls=cmd_util.ApioCommand, short_help="List available board definitions.", help=APIO_BOARDS_HELP, ) diff --git a/apio/commands/apio_build.py b/apio/commands/apio_build.py index c2d3b9fd..3e3bae54 100644 --- a/apio/commands/apio_build.py +++ b/apio/commands/apio_build.py @@ -4,34 +4,32 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio build' command""" import sys from pathlib import Path import click +from apio.utils import cmd_util from apio.managers.scons import SCons from apio.commands import options from apio.apio_context import ApioContext, ApioContextScope -from apio.proto.apio_pb2 import Verbosity +from apio.common.proto.apio_pb2 import Verbosity +# ------------ apio build -# --------------------------- -# -- COMMAND -# --------------------------- - +# -- Text in the markdown format of the python rich library. APIO_BUILD_HELP = """ -The command 'apio build' processes the project’s source files and generates a -bitstream file, which can then be uploaded to your FPGA. +The command 'apio build' processes the project’s source files and generates \ +a bitstream file, which can then be uploaded to your FPGA. -The 'apio build' command compiles all .v files (e.g., my_module.v) in the -project directory, except those whose names end with _tb -(e.g., my_module_tb.v), as these are assumed to be testbenches. +The 'apio build' command compiles all .v files (e.g., my_module.v) in the \ +project directory, except those whose names end with '_tb' \ +(e.g., my_module_tb.v) which are assumed to be testbenches. -\b -Examples: +Examples:[code] apio build # Build - apio build -v # Build with verbose info + apio build -v # Build with verbose info[/code] """ @@ -39,6 +37,7 @@ # pylint: disable=too-many-positional-arguments @click.command( name="build", + cls=cmd_util.ApioCommand, short_help="Synthesize the bitstream.", help=APIO_BUILD_HELP, ) @@ -56,7 +55,7 @@ def cli( verbose_pnr: bool, ): """Implements the apio build command. It invokes the toolchain - to syntesize the source files into a bitstream file. + to synthesize the source files into a bitstream file. """ # The bitstream is generated from the source files (verilog) diff --git a/apio/commands/apio_clean.py b/apio/commands/apio_clean.py index 1663b1e8..4ed452a0 100644 --- a/apio/commands/apio_clean.py +++ b/apio/commands/apio_clean.py @@ -4,7 +4,7 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio clean' command""" import sys @@ -13,24 +13,24 @@ from apio.managers.scons import SCons from apio.commands import options from apio.apio_context import ApioContext, ApioContextScope +from apio.utils import cmd_util -# --------------------------- -# -- COMMAND -# --------------------------- +# ----------- apio clean + +# -- Text in the markdown format of the python rich library. APIO_CLEAN_HELP = """ -The command 'apio clean' removes temporary files generated in the project +The command 'apio clean' removes temporary files generated in the project \ directory by previous Apio commands. -\b -Example: - apio clean - +Example:[code] + apio clean[/code] """ @click.command( name="clean", + cls=cmd_util.ApioCommand, short_help="Delete the apio generated files.", help=APIO_CLEAN_HELP, ) diff --git a/apio/commands/apio_create.py b/apio/commands/apio_create.py index 58d02056..a2782287 100644 --- a/apio/commands/apio_create.py +++ b/apio/commands/apio_create.py @@ -4,7 +4,7 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio create' command""" from pathlib import Path @@ -29,21 +29,19 @@ cls=cmd_util.ApioOption, ) -# --------------------------- -# -- COMMAND -# --------------------------- +# -------------- apio create + +# -- Text in the markdown format of the python rich library. APIO_CREATE_HELP = """ -The command 'apio create' creates a new `apio.ini` project file and is +The command 'apio create' creates a new 'apio.ini' project file and is \ typically used when setting up a new Apio project. -\b -Examples: +Examples:[code] apio create --board alhambra-ii - apio create --board alhambra-ii --top-module MyModule - + apio create --board alhambra-ii --top-module MyModule[/code] -[Note] This command only creates a new 'apio.ini' file, rather than a complete -and buildable project. To create complete projects, refer to the +[b][Note][/b] This command only creates a new 'apio.ini' file, rather than a \ +complete and buildable project. To create complete projects, refer to the \ 'apio examples' command. """ @@ -52,6 +50,7 @@ # pylint: disable=R0913 @click.command( name="create", + cls=cmd_util.ApioCommand, short_help="Create an apio.ini project file.", help=APIO_CREATE_HELP, ) diff --git a/apio/commands/apio_docs.py b/apio/commands/apio_docs.py new file mode 100644 index 00000000..fae64a82 --- /dev/null +++ b/apio/commands/apio_docs.py @@ -0,0 +1,124 @@ +# -*- 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) +# -- License GPLv2 +"""Implementation of 'apio docs' command group.""" + +import click +from rich.table import Table +from rich import box +from apio.utils.cmd_util import ApioGroup, ApioSubgroup, ApioCommand +from apio.apio_context import ApioContext, ApioContextScope +from apio.commands import apio_docs_options +from apio.common.apio_console import cprint, cout, docs_text, PADDING + +# -- apio docs resources + +# -- Text in the markdown format of the python rich library. +APIO_DOCS_RESOURCES_HELP = """ +The command 'apio docs resources' provides information about apio \ +related online resources. + +Examples:[code] + apio docs resources # Provides resources information[/code] +""" + +# -- Text in markdown in rich library format. +APIO_DOCS_RESOURCES_SUMMARY = """ +The table below provides a few Apio and FPGA design-related resources. + +For additional information about specific boards, FPGAs, or tools such as \ +[b]yosys[/] and [b]verible[/], consult their respective documentation. + +[b]Shawn Hymel's[/] excellent video series on YouTube is based on an older \ +version of Apio with a slightly different command set that achieves the \ +same functionality. +""" + + +# R0801: Similar lines in 2 files +# pylint: disable=R0801 +@click.command( + name="resources", + cls=ApioCommand, + short_help="Information about online resources.", + help=APIO_DOCS_RESOURCES_HELP, +) +def _resources_cli(): + """Implements the 'apio docs resources' command.""" + + # -- Create the apio context. We don't really need it here but it also + # -- reads the user preferences and configure the console's colors. + ApioContext(scope=ApioContextScope.NO_PROJECT) + + docs_text(APIO_DOCS_RESOURCES_SUMMARY, width=73) + + # -- Define the table. + table = Table( + show_header=True, + show_lines=True, + padding=PADDING, + box=box.SQUARE, + border_style="dim", + title="Apio Related Resources", + title_justify="left", + ) + + table.add_column("RESOURCE", no_wrap=True) + table.add_column("RESOURCE LOCATION", no_wrap=True, style="cyan") + + # -- Add rows + table.add_row( + "Apio documentation", "https://github.com/FPGAwars/apio/wiki" + ) + + table.add_row( + "Shwan Hymel series", "https://www.youtube.com/watch?v=lLg1AgA2Xoo" + ) + table.add_row("Apio repository", "https://github.com/FPGAwars/apio") + table.add_row( + "Apio requests and bugs", "https://github.com/FPGAwars/apio/issues" + ) + table.add_row("Apio Pypi package", "https://pypi.org/project/apio") + table.add_row("IceStudio (Apio with GUI)", "https://icestudio.io") + table.add_row("FPGAwars (FPGA resources)", "https://fpgawars.github.io") + table.add_row( + "Alhambra-ii FPGA board.", "https://alhambrabits.com/alhambra" + ) + + # -- Render the table. + cout() + cprint(table) + + +# --- apio docs + +# -- Text in the markdown format of the python rich library. +APIO_DOCS_HELP = """ +The command group 'apio docs' contains subcommands that provides various \ +apio documentation and references to online resources. +""" + +# -- We have only a single group with the title 'Subcommands'. +SUBGROUPS = [ + ApioSubgroup( + "Subcommands", + [apio_docs_options.cli, _resources_cli], + ) +] + + +@click.command( + name="docs", + cls=ApioGroup, + subgroups=SUBGROUPS, + short_help="Read apio documentations.", + help=APIO_DOCS_HELP, +) +def cli(): + """Implements the apio docs command.""" + + # pass diff --git a/apio/commands/apio_docs_options.py b/apio/commands/apio_docs_options.py new file mode 100644 index 00000000..ed12b223 --- /dev/null +++ b/apio/commands/apio_docs_options.py @@ -0,0 +1,101 @@ +# -*- 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) +# -- License GPLv2 +"""Implementation of 'apio docs options'.""" + +import sys +import click +from apio.managers import project +from apio.apio_context import ApioContext, ApioContextScope +from apio.utils import cmd_util +from apio.common.apio_console import ( + docs_text, + docs_rule, + cout, + cerror, + cstyle, + DOCS_TITLE, +) + + +# -- apio docs options + +# -- Text in the markdown format of the python rich library. +APIO_DOCS_OPTIONS_HELP = """ +The command 'apio docs options' provides information about the required \ +project file 'apio.ini'. + +Examples:[code] + apio docs options # List an overview and all options. + apio docs options top-module # List a single option.[/code] +""" + +# -- Text in the markdown format of the python rich library. +APIO_INI_DOC = """ +Every Apio project is required to have an 'apio.ini' project configuration \ +file. These are properties text files with '#' comments and a single section \ +called '\\[env]' that contains the required and optional options for this \ +project. + +Example:[code] + \\[env] + board = alhambra-ii # Board id + top-module = my_main # Top module name[/code] + +Following is a list of the apio.ini options and their descriptions. +""" + + +@click.command( + name="options", + cls=cmd_util.ApioCommand, + short_help="Apio.ini options documentation.", + help=APIO_DOCS_OPTIONS_HELP, +) +@click.argument("option", nargs=1, required=False) +def cli( + # Argument + option: str, +): + """Implements the 'apio docs options' command.""" + + # -- Create the apio context. We don't really need it here but it also + # -- reads the user preferences and configure the console's colors. + ApioContext(scope=ApioContextScope.NO_PROJECT) + + # -- If option was specified, validate it. + if option: + if option not in project.OPTIONS: + cerror(f"No such api.ini option: '{option}'") + cout( + "For the list of all apio.ini options, type " + "'apio docs options'.", + style="yellow", + ) + sys.exit(1) + + # -- If printing all the options, print first the overview. + if not option: + docs_text(APIO_INI_DOC) + + # -- Determine options to print + options = [option] if option else project.OPTIONS.keys() + + # -- Print the initial separator line. + docs_rule() + for opt in options: + # -- Print option's title. + is_required = opt in project.REQUIRED_OPTIONS + req = "REQUIRED" if is_required else "OPTIONAL" + styled_option = cstyle(opt.upper(), style=DOCS_TITLE) + cout() + cout(f"{styled_option} ({req})") + + # -- Print the option's text. + text = project.OPTIONS[opt] + docs_text(text) + docs_rule() diff --git a/apio/commands/apio_drivers.py b/apio/commands/apio_drivers.py index 4732e0db..5a9baa2a 100644 --- a/apio/commands/apio_drivers.py +++ b/apio/commands/apio_drivers.py @@ -4,7 +4,7 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio drivers' command group.""" import click @@ -18,8 +18,9 @@ # --- apio drivers +# -- Text in the markdown format of the python rich library. APIO_DRIVERS_HELP = """ -The command group ‘apio drivers’ contains subcommands to manage the +The command group 'apio drivers' contains subcommands to manage the \ drivers on your system. """ @@ -44,6 +45,6 @@ help=APIO_DRIVERS_HELP, ) def cli(): - """Implements the drivers command.""" + """Implements the apio drivers command.""" # pass diff --git a/apio/commands/apio_drivers_install.py b/apio/commands/apio_drivers_install.py index 02acf799..9b2a43a7 100644 --- a/apio/commands/apio_drivers_install.py +++ b/apio/commands/apio_drivers_install.py @@ -4,30 +4,31 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio drivers install' command""" import sys import click from apio.managers.drivers import Drivers from apio.apio_context import ApioContext, ApioContextScope -from apio.utils.cmd_util import ApioGroup, ApioSubgroup +from apio.utils.cmd_util import ApioGroup, ApioSubgroup, ApioCommand # -- apio drivers install ftdi +# -- Text in the markdown format of the python rich library. APIO_DRIVERS_INSTALL_FTDI_HELP = """ -The command 'apio drivers install ftdi' installs on your system the FTDI +The command 'apio drivers install ftdi' installs on your system the FTDI \ drivers required by some FPGA boards. -\b -Examples: - apio drivers install ftdi # Install the ftdi drivers. +Examples:[code] + apio drivers install ftdi # Install the ftdi drivers.[/code] """ @click.command( name="ftdi", + cls=ApioCommand, short_help="Install the ftdi drivers.", help=APIO_DRIVERS_INSTALL_FTDI_HELP, ) @@ -47,18 +48,19 @@ def _ftdi_cli(): # -- apio driver install serial +# -- Text in the markdown format of the python rich library. APIO_DRIVERS_INSTALL_SERIAL_HELP = """ -The command ‘apio drivers install serial’ installs the necessary serial +The command 'apio drivers install serial' installs the necessary serial \ drivers on your system, as required by certain FPGA boards. -\b -Examples: - apio drivers install serial # Install the serial drivers. +Examples:[code] + apio drivers install serial # Install the serial drivers.[/code] """ @click.command( name="serial", + cls=ApioCommand, short_help="Install the serial drivers.", help=APIO_DRIVERS_INSTALL_SERIAL_HELP, ) @@ -71,15 +73,16 @@ def _serial_cli(): # -- Create the drivers manager. drivers = Drivers(apio_ctx) - # Insall + # Install exit_code = drivers.serial_install() sys.exit(exit_code) # --- apio drivers install +# -- Text in the markdown format of the python rich library. APIO_DRIVERS_INSTALL_HELP = """ -The command group 'apio drivers install' includes subcommands that that +The command group 'apio drivers install' includes subcommands that that \ install system drivers that are used to upload designs to FPGA boards. """ diff --git a/apio/commands/apio_drivers_list.py b/apio/commands/apio_drivers_list.py index 01995d7a..48f0f76b 100644 --- a/apio/commands/apio_drivers_list.py +++ b/apio/commands/apio_drivers_list.py @@ -4,35 +4,35 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio drivers list' command""" import sys import click from apio.apio_context import ApioContext, ApioContextScope -from apio.utils.cmd_util import ApioGroup, ApioSubgroup +from apio.utils.cmd_util import ApioGroup, ApioSubgroup, ApioCommand from apio.managers.system import System # -- apio drivers list ftdi - +# -- Text in the markdown format of the python rich library. APIO_DRIVERS_LIST_FTDI_HELP = """ -The command 'apio drivers list ftdi' displays the FTDI devices currently -connected to your computer. It is useful for diagnosing FPGA board +The command 'apio drivers list ftdi' displays the FTDI devices currently \ +connected to your computer. It is useful for diagnosing FPGA board \ connectivity issues. -\b -Examples: - apio drivers list ftdi # List the ftdi devices. +Examples:[code] + apio drivers list ftdi # List the ftdi devices.[/code] -[Hint] This command uses the lsftdi utility, which can also be invoked -directly with the 'apio raw -- lsftdi ' command. +[Hint] This command uses the lsftdi utility, which can also be invoked \ +directly with the 'apio raw -- lsftdi ...' command. """ @click.command( name="ftdi", + cls=ApioCommand, short_help="List the connected ftdi devices.", help=APIO_DRIVERS_LIST_FTDI_HELP, ) @@ -52,21 +52,22 @@ def _ftdi_cli(): # -- apio drivers list serial +# -- Text in the markdown format of the python rich library. APIO_DRIVERS_LIST_SERIAL_HELP = """ -The command ‘apio drivers list serial’ lists the serial devices connected to +The command 'apio drivers list serial' lists the serial devices connected to \ your computer. It is useful for diagnosing FPGA board connectivity issues. -\b -Examples: - apio drivers list serial # List the serial devices. +Examples:[code] + apio drivers list serial # List the serial devices.[/code] -[Hint] This command executes the utility lsserial, which can also be invoked -using the command 'apio raw -- lsserial '. +[b][Hint][/b] This command executes the utility lsserial, which can also be \ +invoked using the command 'apio raw -- lsserial ...'. """ @click.command( name="serial", + cls=ApioCommand, short_help="List the connected serial devices.", help=APIO_DRIVERS_LIST_SERIAL_HELP, ) @@ -86,27 +87,28 @@ def _serial_cli(): # --- apio drivers list usb +# -- Text in the markdown format of the python rich library. APIO_DRIVERS_LIST_USB_HELP = """ -The command ‘apio drivers list usb runs the lsusb utility to list the USB -devices connected to your computer. It is typically used for diagnosing +The command 'apio drivers list usb' runs the lsusb utility to list the USB \ +devices connected to your computer. It is typically used for diagnosing \ connectivity issues with FPGA boards. -\b -Examples: - apio drivers list usb # List the usb devices +Examples:[code] + apio drivers list usb # List the usb devices[/code] -[Hint] You can also run the lsusb utility using the command -'apio raw -- lsusb '. +[b][Hint][/b] You can also run the lsusb utility using the command \ +'apio raw -- lsusb ...'. """ @click.command( name="usb", + cls=ApioCommand, short_help="List connected USB devices.", help=APIO_DRIVERS_LIST_USB_HELP, ) def _usb_cli(): - """Implements the 'apio driverss list usb' command.""" + """Implements the 'apio drivers list usb' command.""" # Create the apio context. apio_ctx = ApioContext(scope=ApioContextScope.NO_PROJECT) @@ -121,8 +123,9 @@ def _usb_cli(): # --- apio drivers list +# -- Text in the markdown format of the python rich library. APIO_DRIVERS_LIST_HELP = """ -The command group 'apio drivers list' includes subcommands that that lists +The command group 'apio drivers list' includes subcommands that that lists \ system drivers that are used with FPGA boards. """ diff --git a/apio/commands/apio_drivers_uninstall.py b/apio/commands/apio_drivers_uninstall.py index 19624203..93bec0c7 100644 --- a/apio/commands/apio_drivers_uninstall.py +++ b/apio/commands/apio_drivers_uninstall.py @@ -4,31 +4,31 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio drivers uninstall' command""" import sys import click from apio.managers.drivers import Drivers from apio.apio_context import ApioContext, ApioContextScope -from apio.utils.cmd_util import ApioGroup, ApioSubgroup +from apio.utils.cmd_util import ApioGroup, ApioSubgroup, ApioCommand # -- apio driver uninstall ftdi +# -- Text in the markdown format of the python rich library. APIO_DRIVERS_UNINSTALL_FTDI_HELP = """ -The command 'apio drivers uninstall ftdi' removes the FTDI drivers that may +The command 'apio drivers uninstall ftdi' removes the FTDI drivers that may \ have been installed earlier. -\b -Examples: - apio drivers uninstall ftdi # Uninstall the ftdi drivers. - +Examples:[code] + apio drivers uninstall ftdi # Uninstall the ftdi drivers.[/code] """ @click.command( name="ftdi", + cls=ApioCommand, short_help="Uninstall the ftdi drivers.", help=APIO_DRIVERS_UNINSTALL_FTDI_HELP, ) @@ -48,18 +48,19 @@ def _ftdi_cli(): # -- apio drivers uninstall serial +# -- Text in the markdown format of the python rich library. APIO_DRIVERS_UNINSTALL_SERIAL_HELP = """ -The command ‘apio drivers uninstall serial’ removes the serial drivers that +The command 'apio drivers uninstall serial' removes the serial drivers that \ you may have installed earlier. -\b -Examples: - apio drivers uinstall serial # Uinstall the serial drivers. +Examples:[code] + apio drivers uninstall serial # Uninstall the serial drivers.[/code] """ @click.command( name="serial", + cls=ApioCommand, short_help="Uninstall the serial drivers.", help=APIO_DRIVERS_UNINSTALL_SERIAL_HELP, ) @@ -79,8 +80,9 @@ def _serial_cli(): # --- apio drivers uninstall +# -- Text in the markdown format of the python rich library. APIO_DRIVERS_UNINSTALL_HELP = """ -The command group 'apio drivers uninstall' includes subcommands that that +The command group 'apio drivers uninstall' includes subcommands that that \ uninstall system drivers that are used to upload designs to FPGA boards. """ diff --git a/apio/commands/apio_examples.py b/apio/commands/apio_examples.py index a4daf602..79408bee 100644 --- a/apio/commands/apio_examples.py +++ b/apio/commands/apio_examples.py @@ -4,48 +4,50 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio examples' command""" from pathlib import Path from typing import List, Any import click -from click import secho, style, echo +from rich.table import Table +from rich import box +from apio.common import apio_console +from apio.common.apio_console import cout, cprint from apio.managers import installer from apio.managers.examples import Examples, ExampleInfo from apio.commands import options from apio.apio_context import ApioContext, ApioContextScope from apio.utils import util -from apio.utils.cmd_util import ApioGroup, ApioSubgroup +from apio.utils.cmd_util import ApioGroup, ApioSubgroup, ApioCommand # ---- apio examples list +# -- Text in the markdown format of the python rich library. APIO_EXAMPLES_LIST_HELP = """ -The command ‘apio examples list’ lists the available Apio project examples +The command 'apio examples list' lists the available Apio project examples \ that you can use. -\b -Examples: +Examples:[code] apio examples list # List all examples - apio examples list -v # List with extra information. - apio examples list | grep alhambra-ii # Show examples of a specific board. - apio examples list | grep -i blink # Show all blinking examples. - - """ + apio examples list -v # More verbose output. + apio examples list | grep alhambra-ii # Show alhambra-ii examples. + apio examples list | grep -i blink # Show blinking examples.[/code] +""" def examples_sort_key(entry: ExampleInfo) -> Any: - """A key for sorting the fpga entries in our prefered order.""" + """A key for sorting the fpga entries in our preferred order.""" return (util.fpga_arch_sort_key(entry.fpga_arch), entry.name) +# R0801: Similar lines in 2 files +# pylint: disable=R0801 def list_examples(apio_ctx: ApioContext, verbose: bool) -> None: """Print all the examples available. Return a process exit code, 0 if ok, non zero otherwise.""" - # -- Get the output info (terminal vs pipe). - output_config = util.get_terminal_config() # -- Make sure that the examples package is installed. installer.install_missing_packages_on_the_fly(apio_ctx) @@ -53,60 +55,68 @@ def list_examples(apio_ctx: ApioContext, verbose: bool) -> None: # -- Get list of examples. entries: List[ExampleInfo] = Examples(apio_ctx).get_examples_infos() - # -- Sort boards by case insensitive board namd. + # -- Sort boards by case insensitive board name. entries.sort(key=examples_sort_key) - # Compute field lengths - margin = 2 - name_len = max(len(x.name) for x in entries) + margin - fpga_arch_len = max(len(x.fpga_arch) for x in entries) + margin - fpga_part_num_len = max(len(x.fpga_part_num) for x in entries) + margin - fpga_size_len = max(len(x.fpga_size) for x in entries) + margin + 1 + # -- Define the table. + table = Table( + show_header=True, + show_lines=False, + box=box.SQUARE, + border_style="dim", + title="Apio Examples", + title_justify="left", + ) - # -- Construct the title fields. - parts = [] - parts.append(f"{'BOARD/EXAMPLE':<{name_len}}") + # -- Add columns. + table.add_column("BOARD/EXAMPLE", no_wrap=True, style="cyan") + table.add_column("ARCH", no_wrap=True) if verbose: - parts.append(f"{'ARCH':<{fpga_arch_len}}") - parts.append(f"{'PART-NUM':<{fpga_part_num_len}}") - parts.append(f"{'SIZE':<{fpga_size_len}}") - parts.append("DESCRIPTION") - - # -- Print the title - secho("".join(parts), fg="cyan", bold="True") + table.add_column("PART-NUM", no_wrap=True) + table.add_column("SIZE", no_wrap=True) + table.add_column( + "DESCRIPTION", + no_wrap=True, + max_width=40 if verbose else 70, # Limit in verbose mode. + ) - # -- Emit the examples + # -- Add rows. last_arch = None for entry in entries: - # -- Seperation before each archictecture group, unless piped out. - if last_arch != entry.fpga_arch and output_config.terminal_mode: - echo("") - secho(f"{entry.fpga_arch.upper()}", fg="magenta", bold=True) + # -- Separation before each architecture group, unless piped out. + if last_arch != entry.fpga_arch and apio_console.is_terminal(): + table.add_section() last_arch = entry.fpga_arch - # -- Construct the fpga fields. - parts = [] - parts.append(style(f"{entry.name:<{name_len}}", fg="cyan")) + # -- Collect row's values. + values = [] + values.append(entry.name) + values.append(entry.fpga_arch) if verbose: - parts.append(f"{entry.fpga_arch:<{fpga_arch_len}}") - parts.append(f"{entry.fpga_part_num:<{fpga_part_num_len}}") - parts.append(f"{entry.fpga_size:<{fpga_size_len}}") - parts.append(f"{entry.description}") + values.append(entry.fpga_part_num) + values.append(entry.fpga_size) + values.append(entry.description) - # -- Print the fpga line. - echo("".join(parts)) + # -- Append the row + table.add_row(*values) - # -- Show summary. - if output_config.terminal_mode: - secho(f"Total of {util.plurality(entries, 'example')}") + # -- Render the table. + cout() + cprint(table) + + # -- Print summary. + if apio_console.is_terminal(): + cout(f"Total of {util.plurality(entries, 'example')}") if not verbose: - secho( - "Run 'apio examples -v' for additional columns.", fg="yellow" + cout( + "Run 'apio examples list -v' for additional columns.", + style="yellow", ) @click.command( name="list", + cls=ApioCommand, short_help="List the available apio examples.", help=APIO_EXAMPLES_LIST_HELP, ) @@ -123,24 +133,22 @@ def _list_cli(verbose: bool): # ---- apio examples fetch - +# -- Text in the markdown format of the python rich library. APIO_EXAMPLES_FETCH_HELP = """ -The command ‘apio examples fetch’ fetches the files of the specified example -to the current directory or to the directory specified by the –dst option. -The destination directory does not need to exist, but if it does, it must be +The command 'apio examples fetch' fetches the files of the specified example \ +to the current directory or to the directory specified by the '-dst' option. \ +The destination directory does not need to exist, but if it does, it must be \ empty. -\b -Examples: +Examples:[code] apio examples fetch alhambra-ii/ledon - apio examples fetch alhambra-ii/ledon -d foo/bar - -[Hint] For the list of available examples, type ‘apio examples list’. + apio examples fetch alhambra-ii/ledon -d foo/bar[/code] """ @click.command( name="fetch", + cls=ApioCommand, short_help="Fetch the files of an example.", help=APIO_EXAMPLES_FETCH_HELP, ) @@ -168,23 +176,22 @@ def _fetch_cli( # ---- apio examples fetch-board - +# -- Text in the markdown format of the python rich library. APIO_EXAMPLES_FETCH_BOARD_HELP = """ -The command ‘apio examples fetch-board’ is used to fetch all the Apio examples -for a specific board. The examples are copied to the current directory or to -the specified destination directory if the –dst option is provided. +The command 'apio examples fetch-board' is used to fetch all the Apio \ +examples for a specific board. The examples are copied to the current \ +directory or to the specified destination directory if the '–-dst' \ +option is provided. -\b -Examples: - apio examples fetch-board alhambra-ii # Fetch to local directory - apio examples fetch-board alhambra-ii -d foo/bar # Fetch to foo/bar +Examples:[code] + apio examples fetch-board alhambra-ii # Fetch board examples. -[Hint] For the list of available examples, type ‘apio examples list’. """ @click.command( name="fetch-board", + cls=ApioCommand, short_help="Fetch all examples of a board.", help=APIO_EXAMPLES_FETCH_BOARD_HELP, ) @@ -215,10 +222,11 @@ def _fetch_board_cli( # ---- apio examples +# -- Text in the markdown format of the python rich library. APIO_EXAMPLES_HELP = """ -The command group ‘apio examples’ provides subcommands for listing and -fetching Apio-provided examples. Each example is a self-contained mini-project -that can be built and uploaded to an FPGA board. +The command group 'apio examples' provides subcommands for listing and \ +fetching Apio-provided examples. Each example is a self-contained \ +mini-project that can be built and uploaded to an FPGA board. """ diff --git a/apio/commands/apio_format.py b/apio/commands/apio_format.py index 71585bee..e71839b7 100644 --- a/apio/commands/apio_format.py +++ b/apio/commands/apio_format.py @@ -4,7 +4,7 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio format' command""" import sys @@ -13,60 +13,57 @@ from glob import glob from typing import Tuple, List import click -from click import secho +from apio.common.apio_console import cout, cerror, cstyle from apio.apio_context import ApioContext, ApioContextScope from apio.commands import options from apio.managers import installer -from apio.utils import util, pkg_util +from apio.utils import util, pkg_util, cmd_util -# --------------------------- -# -- COMMAND -# --------------------------- +# -------------- apio format + +# -- Text in the markdown format of the python rich library. APIO_FORMAT_HELP = """ -The command ‘apio format’ formats Verilog source files to ensure consistency -and style without altering their semantics. The command accepts the names of -pecific source files to format or formats all project source files by default. +The command 'apio format' formats Verilog source files to ensure consistency \ +and style without altering their semantics. The command accepts the names of \ +specific source files to format or formats all project source files by default. -\b -Examples: +Examples:[code] apio format # Format all source files. - apio format -v # Same as above but with verbose output. - apio format main.v main_tb.v # Format the two tiven files. + apio format -v # Same but with verbose output. + apio format main.v main_tb.v # Format the two files.[/code] -The format command utilizes the format tool from the Verible project, which -can be configured by setting its flags in the apio.ini project file +The format command utilizes the format tool from the Verible project, which \ +can be configured by setting its flags in the apio.ini project file \ For example: -\b -format-verible-options = + +[code]format-verible-options = --column_limit=80 - --indentation_spaces=4 + --indentation_spaces=4[/code] -If needed, sections of source code can be protected from formatting using +If needed, sections of source code can be protected from formatting using \ Verible formatter directives: -\b -// verilog_format: off +[code]// verilog_format: off ... untouched code ... -// verilog_format: on +// verilog_format: on[/code] -For a full list of Verible formatter flags, refer to the documentation page +For a full list of Verible formatter flags, refer to the documentation page \ online or use the command 'apio raw -- verible-verilog-format --helpful'. """ @click.command( name="format", + cls=cmd_util.ApioCommand, short_help="Format verilog source files.", help=APIO_FORMAT_HELP, ) -@click.pass_context @click.argument("files", nargs=-1, required=False) @options.project_dir_option @options.verbose_option def cli( - _cmd_ctx: click.Context, # Arguments files: Tuple[str], project_dir: Path, @@ -97,7 +94,7 @@ def cli( # -- Convert the tuple with file names into a list. files: List[str] = list(files) - # -- If user didn't specify files to firmat, all all source files to + # -- If user didn't specify files to format, all all source files to # -- the list. if not files: files.extend(glob(str(apio_ctx.project_dir / "*.v"))) @@ -105,7 +102,7 @@ def cli( # -- Error if no file to format. if not files: - secho("Error: No '.v' or '.sv' files to format", fg="red") + cerror("No '.v' or '.sv' files to format") sys.exit(1) # -- Sort files, case insensitive. @@ -121,21 +118,19 @@ def cli( # -- Check the file extension. _, ext = os.path.splitext(path) if ext not in [".v", ".sv"]: - secho( - f"Error: '{f}' has an invalid extension, " - "should be '.v' or '.sv'", - fg="red", + cerror( + f"'{f}' has an invalid extension, " "should be '.v' or '.sv'" ) sys.exit(1) # -- Check that the file exists and is a file. if not path.is_file(): - secho(f"Error: '{f}' is not a file.", fg="red") + cerror(f"'{f}' is not a file.") sys.exit(1) # -- Print file name. - styled_f = click.style(f, fg="magenta") - secho(f"Formatting {styled_f}") + styled_f = cstyle(f, style="magenta") + cout(f"Formatting {styled_f}") # -- Construct the formatter command line. command = ( @@ -143,14 +138,14 @@ def cli( f' {" ".join(cmd_options)} "{f}"' ) if verbose: - secho(command) + cout(command) # -- Execute the formatter command line. exit_code = os.system(command) if exit_code != 0: - secho(f"Error: formatting of '{f}' failed", fg="red") + cerror(f"Formatting of '{f}' failed") return exit_code # -- All done ok. - secho(f"Formatted {util.plurality(files, 'file')}.", fg="green", bold=True) + cout(f"Formatted {util.plurality(files, 'file')}.", style="green") sys.exit(0) diff --git a/apio/commands/apio_fpgas.py b/apio/commands/apio_fpgas.py index d3d30257..617aeeee 100644 --- a/apio/commands/apio_fpgas.py +++ b/apio/commands/apio_fpgas.py @@ -4,7 +4,7 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio fpgas' command""" import sys @@ -12,9 +12,12 @@ from dataclasses import dataclass from typing import List, Dict import click -from click import secho, style, echo +from rich.table import Table +from rich import box +from apio.common import apio_console +from apio.common.apio_console import cout, cprint from apio.apio_context import ApioContext, ApioContextScope -from apio.utils import util +from apio.utils import util, cmd_util from apio.commands import options @@ -35,7 +38,7 @@ class Entry: fpga_speed: str def sort_key(self): - """A key for sorting the fpga entries in our prefered order.""" + """A key for sorting the fpga entries in our preferred order.""" return (util.fpga_arch_sort_key(self.fpga_arch), self.fpga.lower()) @@ -44,9 +47,6 @@ def sort_key(self): def list_fpgas(apio_ctx: ApioContext, verbose: bool): """Prints all the available FPGA definitions.""" - # -- Get the output info (terminal vs pipe). - output_config = util.get_terminal_config() - # -- Collect a sparse dict with fpga ids to board count. boards_counts: Dict[str, int] = {} for board_info in apio_ctx.boards.values(): @@ -80,89 +80,86 @@ def list_fpgas(apio_ctx: ApioContext, verbose: bool): ) ) - # -- Sort boards by our prefered order. + # -- Sort boards by our preferred order. entries.sort(key=lambda x: x.sort_key()) - # -- Compute field lengths - margin = 3 - fpga_len = max(len(x.fpga) for x in entries) + margin - board_count_len = 6 + margin - fpga_arch_len = max(len(x.fpga_arch) for x in entries) + margin - fpga_part_num_len = max(len(x.fpga_part_num) for x in entries) + margin - fpga_size_len = max(len(x.fpga_size) for x in entries) + margin - fpga_type_len = max(len(x.fpga_type) for x in entries) + margin - fpga_pack_len = max(len(x.fpga_pack) for x in entries) + margin - fpga_speed_len = 5 + margin - - # -- Construct the title fields. - parts = [] - parts.append(f"{'FPGA-ID':<{fpga_len}}") - parts.append(f"{'BOARDS':<{board_count_len}}") - parts.append(f"{'ARCH':<{fpga_arch_len}}") - parts.append(f"{'PART-NUMBER':<{fpga_part_num_len}}") - parts.append(f"{'SIZE':<{fpga_size_len}}") - if verbose: - parts.append(f"{'TYPE':<{fpga_type_len}}") - parts.append(f"{'PACK':<{fpga_pack_len}}") - parts.append(f"{'SPEED':<{fpga_speed_len}}") + # -- Define the table. + table = Table( + show_header=True, + show_lines=False, + box=box.SQUARE, + border_style="dim", + title="Apio Supported FPGAs", + title_justify="left", + ) - # -- Print the title - secho("".join(parts), fg="cyan", bold="True") + # -- Add columns + table.add_column("FPGA-ID", no_wrap=True, style="cyan") + table.add_column("BOARDS", no_wrap=True, justify="center") + table.add_column("ARCH", no_wrap=True) + table.add_column("PART-NUMBER", no_wrap=True) + table.add_column("SIZE", no_wrap=True, justify="right") + if verbose: + table.add_column("TYPE", no_wrap=True) + table.add_column("PACK", no_wrap=True) + table.add_column("SPEED", no_wrap=True, justify="center") - # -- Iterate and print the fpga entries in the list. + # -- Add rows. last_arch = None for entry in entries: - # -- Seperation before each archictecture group, unless piped out. - if last_arch != entry.fpga_arch and output_config.terminal_mode: - echo("") - secho(f"{entry.fpga_arch.upper()}", fg="magenta", bold=True) + # -- If switching architecture, add an horizontal separation line. + if last_arch != entry.fpga_arch and apio_console.is_terminal(): + table.add_section() last_arch = entry.fpga_arch - # -- Construct the fpga fields. - parts = [] - parts.append(style(f"{entry.fpga:<{fpga_len}}", fg="cyan")) - board_count = f"{entry.board_count:>3}" if entry.board_count else "" - parts.append(f"{board_count:<{board_count_len}}") - parts.append(f"{entry.fpga_arch:<{fpga_arch_len}}") - parts.append(f"{entry.fpga_part_num:<{fpga_part_num_len}}") - parts.append(f"{entry.fpga_size:<{fpga_size_len}}") + # -- Collect row values. + values = [] + values.append(entry.fpga) + values.append(f"{entry.board_count:>2}" if entry.board_count else "") + values.append(entry.fpga_arch) + values.append(entry.fpga_part_num) + values.append(entry.fpga_size) if verbose: - parts.append(f"{entry.fpga_type:<{fpga_type_len}}") - parts.append(f"{entry.fpga_pack:<{fpga_pack_len}}") - parts.append(f"{entry.fpga_speed:<{fpga_speed_len}}") + values.append(entry.fpga_type) + values.append(entry.fpga_pack) + values.append(entry.fpga_speed) - # -- Print the fpga line. - echo("".join(parts)) + # -- Add row. + table.add_row(*values) + + # -- Render the table. + cout() + cprint(table) # -- Show summary. - if output_config.terminal_mode: - secho(f"Total of {util.plurality(entries, 'fpga')}") + if apio_console.is_terminal(): + cout(f"Total of {util.plurality(entries, 'fpga')}") if not verbose: - secho("Run 'apio fpgas -v' for additional columns.", fg="yellow") + cout("Run 'apio fpgas -v' for additional columns.", style="yellow") + +# -------- apio fpgas -# --------------------------- -# -- COMMAND -# --------------------------- # R0801: Similar lines in 2 files # pylint: disable = R0801 + +# -- Text in the markdown format of the python rich library. APIO_FPGAS_HELP = """ -The command ‘apio fpgas’ lists the FPGAs recognized by Apio. Custom FPGAs -supported by the underlying Yosys toolchain can be defined by placing a -custom fpgas.jsonc file in the project directory, overriding Apio’s standard -fpgas.jsonc file. +The command 'apio fpgas' lists the FPGAs recognized by Apio. Custom FPGAs \ +supported by the underlying Yosys toolchain can be defined by placing a \ +custom 'fpgas.jsonc' file in the project directory, overriding Apio’s \ +standard 'fpgas.jsonc' file. -\b -Examples: +Examples:[code] apio fpgas # List all fpgas. apio fpgas -v # List with extra columns. - apio fpgas | grep gowin # Filter FPGA results. - + apio fpgas | grep gowin # Filter FPGA results.[/code] """ @click.command( name="fpgas", + cls=cmd_util.ApioCommand, short_help="List available FPGA definitions.", help=APIO_FPGAS_HELP, ) diff --git a/apio/commands/apio_graph.py b/apio/commands/apio_graph.py index b4fae813..dc45f680 100644 --- a/apio/commands/apio_graph.py +++ b/apio/commands/apio_graph.py @@ -4,23 +4,21 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio graph' command""" import sys from pathlib import Path import click from apio.managers.scons import SCons -from apio.utils import cmd_util from apio.commands import options from apio.apio_context import ApioContext, ApioContextScope from apio.utils.util import nameof -from apio.proto.apio_pb2 import GraphOutputType, GraphParams, Verbosity +from apio.utils import cmd_util +from apio.common.proto.apio_pb2 import GraphOutputType, GraphParams, Verbosity -# --------------------------- -# -- COMMAND SPECIFIC OPTIONS -# --------------------------- +# ---------- apio graph svg_option = click.option( "svg", # Var name. @@ -46,25 +44,21 @@ cls=cmd_util.ApioOption, ) - -# --------------------------- -# -- COMMAND -# --------------------------- +# -- Text in the markdown format of the python rich library. APIO_GRAPH_HELP = """ -The command ‘apio graph’ generates a graphical representation of the Verilog +The command 'apio graph' generates a graphical representation of the Verilog \ code in the project. -\b -Examples: +Examples:[code] apio graph # Generate a svg file. apio graph --svg # Generate a svg file. apio graph --pdf # Generate a pdf file. apio graph --png # Generate a png file. - apio graph -t my_module # Graph my_module module. + apio graph -t my_module # Graph my_module module.[/code] -[Hint] On Windows, type ‘explorer _build/hardware.svg’ to view the graph, -and on Mac OS type ‘open _build/hardware.svg’. +[b][Hint][/b] On Windows, type 'explorer _build/hardware.svg' to view the \ +graph, and on Mac OS type 'open _build/hardware.svg'. """ @@ -72,6 +66,7 @@ # pylint: disable=too-many-positional-arguments @click.command( name="graph", + cls=cmd_util.ApioCommand, short_help="Generate a visual graph of the code.", help=APIO_GRAPH_HELP, ) diff --git a/apio/commands/apio_lint.py b/apio/commands/apio_lint.py index 94413ba4..4836705e 100644 --- a/apio/commands/apio_lint.py +++ b/apio/commands/apio_lint.py @@ -4,7 +4,7 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio lint' command""" import sys from pathlib import Path @@ -14,12 +14,10 @@ from apio.utils import cmd_util from apio.commands import options from apio.apio_context import ApioContext, ApioContextScope -from apio.proto.apio_pb2 import LintParams +from apio.common.proto.apio_pb2 import LintParams -# --------------------------- -# -- COMMAND SPECIFIC OPTIONS -# --------------------------- +# ------- apio lint all_option = click.option( "all_", # Var name. Deconflicting from Python'g builtin 'all'. @@ -58,19 +56,16 @@ ) -# --------------------------- -# -- COMMAND -# --------------------------- +# -- Text in the markdown format of the python rich library. APIO_LINT_HELP = """ -The command ‘apio lint’ scans the project’s Verilog code and reports errors, -inconsistencies, and style violations. The command uses the Verilator tool, +The command 'apio lint' scans the project's Verilog code and reports errors, \ +inconsistencies, and style violations. The command uses the Verilator tool, \ which is included in the standard Apio installation. -\b -Examples: +Examples:[code] apio lint apio lint -t my_module - apio lint --all + apio lint --all[/code] """ @@ -78,26 +73,27 @@ # pylint: disable=too-many-positional-arguments @click.command( name="lint", + cls=cmd_util.ApioCommand, short_help="Lint the verilog code.", help=APIO_LINT_HELP, ) @click.pass_context -@options.top_module_option_gen( - help="Restrict linting to this module and its depedencies." -) -@all_option @nostyle_option @nowarn_option @warn_option +@all_option +@options.top_module_option_gen( + help="Restrict linting to this module and its dependencies." +) @options.project_dir_option def cli( _: click.Context, # Options - top_module: str, - all_: bool, nostyle: bool, nowarn: str, warn: str, + all_: bool, + top_module: str, project_dir: Path, ): """Lint the verilog code.""" @@ -111,7 +107,7 @@ def cli( # -- Create the scons manager. scons = SCons(apio_ctx) - # -- Convert the comma seperated args values to python lists + # -- Convert the comma separated args values to python lists no_warns_list = util.split(nowarn, ",", strip=True, keep_empty=False) warns_list = util.split(warn, ",", strip=True, keep_empty=False) diff --git a/apio/commands/apio_packages.py b/apio/commands/apio_packages.py index 44379308..c7d1ce71 100644 --- a/apio/commands/apio_packages.py +++ b/apio/commands/apio_packages.py @@ -4,40 +4,145 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio packages' command""" from typing import Tuple import click -from click import secho +from rich.table import Table +from rich import box +from apio.common.apio_console import cout, cprint from apio.managers import installer from apio.apio_context import ApioContext, ApioContextScope from apio.utils import pkg_util from apio.commands import options -from apio.utils.cmd_util import ApioGroup, ApioSubgroup +from apio.utils.cmd_util import ApioGroup, ApioSubgroup, ApioCommand + + +def print_packages_report(apio_ctx: ApioContext) -> None: + """A common function to print the state of the packages.""" + + # -- Scan the packages + scan = pkg_util.scan_packages( + apio_ctx, cached_config_ok=False, verbose=False + ) + + # -- Shortcuts to reduce clutter. + get_package_version = apio_ctx.profile.get_package_installed_version + get_package_info = apio_ctx.get_package_info + + table = Table( + show_header=True, + show_lines=True, + box=box.SQUARE, + border_style="dim", + title="Apio Packages Status", + title_justify="left", + padding=(0, 2), + ) + + table.add_column("PACKAGE NAME", no_wrap=True) + table.add_column("VERSION", no_wrap=True) + table.add_column("DESCRIPTION", no_wrap=True) + table.add_column("STATUS", no_wrap=True) + + # -- Add raws for installed ok packages. + for package_name in scan.installed_ok_package_names: + version = get_package_version(package_name) + description = get_package_info(package_name)["description"] + table.add_row(package_name, version, description, "OK") + + # -- Add rows for uninstalled packages. + for package_name in scan.uninstalled_package_names: + description = get_package_info(package_name)["description"] + table.add_row( + package_name, None, description, "Uninstalled", style="yellow" + ) + + # -- Add raws for installed with version mismatch packages. + for package_name in scan.bad_version_package_names: + version = get_package_version(package_name) + description = get_package_info(package_name)["description"] + table.add_row( + package_name, version, description, "Wrong version", style="red" + ) + + # -- Add rows for broken packages. + for package_name in scan.broken_package_names: + description = get_package_info(package_name)["description"] + table.add_row(package_name, None, description, "Broken", style="red") + + # -- Render table. + cout() + cprint(table) + + # -- Define errors table. + table = Table( + show_header=True, + show_lines=True, + box=box.SQUARE, + border_style="dim", + title="Apio Packages Errors", + title_justify="left", + padding=(0, 2), + ) + + # -- Add columns. + table.add_column("ERROR TYPE", no_wrap=True, min_width=15, style="red") + table.add_column("NAME", no_wrap=True, min_width=15) + + # -- Add rows. + for package_name in scan.orphan_package_names: + table.add_row("Orphan package", package_name) + + for name in sorted(scan.orphan_dir_names): + table.add_row("Orphan dir", name) + + for name in sorted(scan.orphan_file_names): + table.add_row("Orphan file", name) + + # -- Render the table, unless empty. + if table.row_count: + cout() + cprint(table) + + # -- Print summary. + cout() + if not scan.packages_installed_ok(): + cout( + "Run 'apio packages install' to install all packages.", + style="yellow", + ) + elif scan.num_errors_to_fix(): + cout( + "Run 'apio packages fix' to fix the errors.", + style="yellow", + ) + else: + cout("All Apio packages are installed OK.", style="green") # ------ apio packages install +# -- Text in the markdown format of the python rich library. APIO_PACKAGES_INSTALL_HELP = """ -The command ‘apio packages install’ installs Apio packages that are required +The command 'apio packages install' installs Apio packages that are required \ for the operation of Apio on your system. +Examples:[code] + apio packages install # Install missing packages. + apio packages install --force # Reinstall all packages. + apio packages install oss-cad-suite # Install package. + apio packages install examples@0.0.32 # Install a specific version.[/code] -\b -Examples: - apio packages install # Install all missing packages. - apio packages install --force # Re/install all missing packages. - apio packages install oss-cad-suite # Install just this package. - apio packages install examples@0.0.32 # Install a specific version. - -Adding the --force option forces the reinstallation of existing packages; +Adding the '--force' option forces the reinstallation of existing packages; \ otherwise, packages that are already installed correctly remain unchanged. """ @click.command( name="install", + cls=ApioCommand, short_help="Install apio packages.", help=APIO_PACKAGES_INSTALL_HELP, ) @@ -55,40 +160,52 @@ def _install_cli( apio_ctx = ApioContext(scope=ApioContextScope.NO_PROJECT) - secho(f"Platform id '{apio_ctx.platform_id}'") + cout(f"Platform id '{apio_ctx.platform_id}'") - # -- If packages where specified, install all packages that are valid - # -- for this platform. + # -- First thing, fix broken packages, if any. + installer.scan_and_fix_packages( + apio_ctx, cached_config_ok=False, verbose=verbose + ) + + # -- If packages where specified, install all the missing ones, if any. + scan = pkg_util.scan_packages( + apio_ctx, cached_config_ok=False, verbose=False + ) if not packages: packages = apio_ctx.platform_packages.keys() # -- Install the packages, one by one. for package in packages: - installer.install_package( - apio_ctx, - package_spec=package, - force_reinstall=force, - cached_config_ok=False, - verbose=verbose, - ) + if force or package not in scan.installed_ok_package_names: + installer.install_package( + apio_ctx, + package_spec=package, + force_reinstall=force, + cached_config_ok=False, + verbose=verbose, + ) + + # -- Scan the available and installed packages. + print_packages_report(apio_ctx) # ------ apio packages uninstall +# -- Text in the markdown format of the python rich library. APIO_PACKAGES_UNINSTALL_HELP = """ -The command ‘apio packages uninstall’ removes installed Apio packages from +The command 'apio packages uninstall' removes installed Apio packages from \ your system. The command does not uninstall the Apio tool itself. -\b -Examples: - apio packages uninstall # Uninstall all packages - apio packages uninstall oss-cad-suite # Uninstall a package - apio packages uninstall oss-cad-suite examples # Uninstall two packages +Examples:[code] + apio packages uninstall # Uninstall all packages + apio packages uninstall oss-cad-suite # Uninstall a package + apio packages uninstall verible examples # Uninstall two packages[/code] """ @click.command( name="uninstall", + cls=ApioCommand, short_help="Uninstall apio packages.", help=APIO_PACKAGES_UNINSTALL_HELP, ) @@ -104,33 +221,48 @@ def _uninstall_cli( apio_ctx = ApioContext(scope=ApioContextScope.NO_PROJECT) - # -- If packages where specified, uninstall all packages that are valid - # -- for this platform. + # -- First thing, fix broken packages, if any. + installer.scan_and_fix_packages( + apio_ctx, cached_config_ok=False, verbose=verbose + ) + + # -- Scan the packages. + scan = pkg_util.scan_packages( + apio_ctx, cached_config_ok=False, verbose=False + ) + + # -- If packages where specified, uninstall all packages. if not packages: packages = apio_ctx.platform_packages.keys() # -- Uninstall the packages. for package in packages: - installer.uninstall_package( - apio_ctx, package_spec=package, verbose=verbose - ) + # -- Skip packages that are alredy uninstalled. + if package not in scan.uninstalled_package_names: + installer.uninstall_package( + apio_ctx, package_spec=package, verbose=verbose + ) + + # -- Print updated package report. + print_packages_report(apio_ctx) # ------ apio packages list +# -- Text in the markdown format of the python rich library. APIO_PACKAGES_LIST_HELP = """ -The command ‘apio packages list’ lists the available and installed Apio -packages. The list of available packages depends on the operating system +The command 'apio packages list' lists the available and installed Apio \ +packages. The list of available packages depends on the operating system \ you are using and may vary between operating systems. -\b -Examples: - apio packages list +Examples:[code] + apio packages list[/code] """ @click.command( name="list", + cls=ApioCommand, short_help="List apio packages.", help=APIO_PACKAGES_LIST_HELP, ) @@ -139,41 +271,25 @@ def _list_cli(): apio_ctx = ApioContext(scope=ApioContextScope.NO_PROJECT) - # -- Scan the available and installed packages. - scan = pkg_util.scan_packages( - apio_ctx, cached_config_ok=False, verbose=False - ) - - # -- List the findings. - pkg_util.list_packages(apio_ctx, scan) - - # -- Print an hint or summary based on the findings. - if scan.num_errors_to_fix(): - secho("[Hint] run 'apio packages fix' to fix the errors.", fg="yellow") - elif scan.uninstalled_package_names: - secho( - "[Hint] run 'apio packages install' to install all " - "available packages.", - fg="yellow", - ) - else: - secho("All packages are installed.", fg="green", bold=True) + # -- Print packages report. + print_packages_report(apio_ctx) # ------ apio packages fix +# -- Text in the markdown format of the python rich library. APIO_PACKAGES_FIX_HELP = """ -The command ‘apio packages fix’ removes broken or obsolete packages -that are listed as broken by the command ‘apio packages list’. +The command 'apio packages fix' removes broken or obsolete packages \ +that are listed as broken by the command 'apio packages list'. -\b -Examples: - apio packages fix # Fix package errors, if any. +Examples:[code] + apio packages fix # Fix package errors, if any.[/code] """ @click.command( name="fix", + cls=ApioCommand, short_help="Fix broken apio packages.", help=APIO_PACKAGES_FIX_HELP, ) @@ -183,37 +299,26 @@ def _fix_cli(): # -- Create the apio context. apio_ctx = ApioContext(scope=ApioContextScope.NO_PROJECT) - # -- Scan the availeable and installed packages. - scan = pkg_util.scan_packages( + # -- First thing, fix broken packages, if any. + installer.scan_and_fix_packages( apio_ctx, cached_config_ok=False, verbose=False ) - # -- Fix any errors. - if scan.num_errors_to_fix(): - installer.fix_packages(apio_ctx, scan) - else: - secho("No errors to fix") - - # -- Show the new state - new_scan = pkg_util.scan_packages( - apio_ctx, cached_config_ok=True, verbose=False - ) - pkg_util.list_packages(apio_ctx, new_scan) + # -- Print updated packages report. + print_packages_report(apio_ctx) # ------ apio packages (group) - +# -- Text in the markdown format of the python rich library. APIO_PACKAGES_HELP = """ -The command group ‘apio packages’ provides commands to manage the installation -of Apio packages. These are not Python packages but Apio-specific packages -containing various tools and data essential for the operation of Apio. -These packages are installed after the installation of the Apio Python package -itself, using the command ‘apio packages install’. - -The list of available -packages depends on the operating system you are using and may vary between -different operating systems. +The command group 'apio packages' provides commands to manage the \ +installation of Apio packages. These are not Python packages but \ +Apio-specific packages containing various tools and data essential for the \ +operation of Apio. + +The list of available packages depends on the operating system you are \ +using and may vary between different operating systems. """ diff --git a/apio/commands/apio_preferences.py b/apio/commands/apio_preferences.py index decd6506..0c766d1b 100644 --- a/apio/commands/apio_preferences.py +++ b/apio/commands/apio_preferences.py @@ -4,30 +4,33 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio preferences' command""" import click -from click import secho, echo, style +from rich.table import Table +from rich import box +from apio.common.apio_console import cout, cprint from apio.utils import cmd_util from apio.apio_context import ApioContext, ApioContextScope -from apio.utils.cmd_util import ApioGroup, ApioSubgroup +from apio.utils.cmd_util import ApioGroup, ApioSubgroup, ApioCommand # ---- apio preferences list - +# -- Text in the markdown format of the python rich library. APIO_PREFERENCES_LIST_HELP = """ -The command ‘apio preferences list’ lists the current user preferences. - -\b -Examples: - apio preferences list # List the user preferences. +The command 'apio preferences list' lists the current user preferences. - """ +Examples:[code] + apio preferences list # List the user preferences.[/code] +""" +# R0801: Similar lines in 2 files +# pylint: disable=R0801 @click.command( name="list", + cls=ApioCommand, short_help="List the apio user preferences.", help=APIO_PREFERENCES_LIST_HELP, ) @@ -37,26 +40,39 @@ def _list_cli(): # -- Create the apio context. apio_ctx = ApioContext(scope=ApioContextScope.NO_PROJECT) - # -- Print title. - secho("Apio user preferences:", fg="magenta") + table = Table( + show_header=True, + show_lines=True, + box=box.SQUARE, + border_style="dim", + title="Apio User Preferences", + title_justify="left", + padding=(0, 2), + ) - # -- Show colors preference. + # -- Add columns. + table.add_column("ITEM", no_wrap=True) + table.add_column("VALUE", no_wrap=True, style="cyan", min_width=30) + + # -- Add rows. value = apio_ctx.profile.preferences.get("colors", "on") - styled_value = style(value, fg="cyan", bold=True) - echo(f"Colors: {styled_value}") + table.add_row("Colors", value) + # -- Render table. + cout() + cprint(table) -# ---- apio preferences set +# ---- apio preferences set +# -- Text in the markdown format of the python rich library. APIO_PREF_SET_HELP = """ -The command ‘apio preferences set' allows to set the supported user +The command 'apio preferences set' allows to set the supported user \ preferences. -\b -Examples: - apio preferences set --colors yes # Select multi-color output. - apio preferences set --colors no # Select monochrome output. +Examples:[code] + apio preferences set --colors on # Enable colors. + apio preferences set --colors off # Disable colors.[/code] The apio colors are optimized for a terminal windows with a white background. """ @@ -74,6 +90,7 @@ def _list_cli(): @click.command( name="set", + cls=ApioCommand, short_help="Set the apio user preferences.", help=APIO_PREF_SET_HELP, ) @@ -89,18 +106,19 @@ def _set_cli(colors: str): # -- Show the result. The new colors preference is already in effect. color = apio_ctx.profile.preferences["colors"] - secho(f"Colors set to [{color}]", fg="green", bold=True) + cout(f"Colors set to [{color}]", style="green") # --- apio preferences +# -- Text in the markdown format of the python rich library. APIO_PREFERENCES_HELP = """ -The command group ‘apio preferences' contains subcommands to manage -the apio user preferences. These are user configurations that affect all the -apio project on the same computer. +The command group 'apio preferences' contains subcommands to manage \ +the apio user preferences. These are user configurations that affect all the \ +apio projects that use the same apio home directory (e.g. '~/.apio'). -The user preference is not part of any apio project and typically are not -shared when multiple user colaborate on the same project. +The user preference is not part of any apio project and typically are not \ +shared when multiple user collaborate on the same project. """ # -- We have only a single group with the title 'Subcommands'. diff --git a/apio/commands/apio_raw.py b/apio/commands/apio_raw.py index 9d397162..31df0cc6 100644 --- a/apio/commands/apio_raw.py +++ b/apio/commands/apio_raw.py @@ -4,51 +4,51 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio raw' command""" import sys import subprocess from typing import Tuple, List import click -from click import secho +from apio.common.apio_console import cout from apio.apio_context import ApioContext, ApioContextScope from apio.commands import options from apio.utils import cmd_util, pkg_util +from apio.utils.cmd_util import ApioCommand from apio.utils.util import nameof from apio.managers import installer -# --------------------------- -# -- COMMAND -# --------------------------- +# ----------- apio raw + +# -- Text in the markdown format of the python rich library. APIO_RAW_HELP = """ -The command ‘apio raw’ allows you to bypass Apio and run underlying tools -directly. This is an advanced command that requires familiarity with the +The command 'apio raw' allows you to bypass Apio and run underlying tools \ +directly. This is an advanced command that requires familiarity with the \ underlying tools. -Before running the command, Apio temporarily modifies system environment -variables such as $PATH to provide access to its packages. To view these -environment changes, run the command with the -v option. - -\b -Examples: - apio raw -- yosys --version # Yosys version - apio raw -v -- yosys --version # Same but with verbose apio info. - apio raw -- yosys # Run Yosys in interactive mode. - apio raw -- icepll -i 12 -o 30 # Calc ICE PLL - apio raw -v # Show apio env setting. - apio raw -h # Show this help info. - -The -- token is used to separate Apio commands and their arguments from the -underlying tools and their arguments. It can be omitted in some cases, but -it’s a good practice to always use it. As a rule of thumb, always prefix the -raw command you want to run with 'apio raw -- '. +Before running the command, Apio temporarily modifies system environment \ +variables such as '$PATH' to provide access to its packages. To view these \ +environment changes, run the command with the '-v' option. + +Examples:[code] + apio raw -- yosys --version # Yosys version + apio raw -v -- yosys --version # Verbose apio info. + apio raw -- yosys # Yosys interactive mode. + apio raw -- icepll -i 12 -o 30 # Calc ICE PLL. + apio raw -- which yosys # Lookup a command. + apio raw -v # Show apio env setting. + apio raw -h # Show this help info.[/code] + +The '--' marker is used to separate between the arguments of the apio \ +command itself and those of the executed command. """ @click.command( name="raw", + cls=ApioCommand, short_help="Execute commands directly from the Apio packages.", help=APIO_RAW_HELP, context_settings={"ignore_unknown_options": True}, @@ -90,22 +90,22 @@ def cli( # -- Echo the commands. The apio raw command is platform dependent # -- so this may help us and the user diagnosing issues. if verbose: - secho(f"\n---- Executing {cmd}:") + cout(f"\n---- Executing {cmd}:") # -- Invoke the command. try: exit_code = subprocess.call(cmd, shell=False) except FileNotFoundError as e: - secho(f"{e}", fg="red") + cout(f"{e}", style="red") sys.exit(1) if verbose: - secho("----\n") + cout("----\n") if exit_code == 0: - secho("Exit status [0] OK", fg="green", bold=True) + cout("Exit status [0] OK", style="green") else: - secho(f"Exist status [{exit_code}] ERROR", fg="red") + cout(f"Exist status [{exit_code}] ERROR", style="red") # -- Return the command's status code. sys.exit(exit_code) diff --git a/apio/commands/apio_report.py b/apio/commands/apio_report.py index 8d18b5e0..275991b3 100644 --- a/apio/commands/apio_report.py +++ b/apio/commands/apio_report.py @@ -4,7 +4,7 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio' report' command""" import sys @@ -13,22 +13,21 @@ from apio.managers.scons import SCons from apio.commands import options from apio.apio_context import ApioContext, ApioContextScope -from apio.proto.apio_pb2 import Verbosity +from apio.common.proto.apio_pb2 import Verbosity +from apio.utils import cmd_util -# --------------------------- -# -- COMMAND -# --------------------------- +# ---------- apio report + +# -- Text in the markdown format of the python rich library. APIO_REPORT_HELP = """ -The command ‘apio report’ provides information on the utilization and timing -of the design. It is useful for analyzing utilization bottlenecks and +The command 'apio report' provides information on the utilization and timing \ +of the design. It is useful for analyzing utilization bottlenecks and \ verifying that the design can operate at the desired clock speed. -\b -Examples: - apio report - epio report --verbose - +Examples:[code] + apio report # Print report. + apio report --verbose # Print extra information.[/code] """ @@ -37,6 +36,7 @@ # pylint: disable=too-many-positional-arguments @click.command( name="report", + cls=cmd_util.ApioCommand, short_help="Report design utilization and timing.", help=APIO_REPORT_HELP, ) diff --git a/apio/commands/apio_sim.py b/apio/commands/apio_sim.py index c0a017ae..5f968ced 100644 --- a/apio/commands/apio_sim.py +++ b/apio/commands/apio_sim.py @@ -4,56 +4,57 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio sim' command""" import sys from pathlib import Path import click -from click import secho +from apio.common.apio_console import cout from apio.managers.scons import SCons from apio.commands import options from apio.apio_context import ApioContext, ApioContextScope -from apio.proto.apio_pb2 import SimParams +from apio.common.proto.apio_pb2 import SimParams +from apio.utils import cmd_util -# --------------------------- -# -- COMMAND -# --------------------------- +# --------- apio sim +# -- Text in the markdown format of the python rich library. APIO_SIM_HELP = """ -The command ‘apio sim’ simulates the default or the specified testbench file -and displays its simulation results in a graphical GTKWave window. -The testbench is expected to have a name ending with _tb, such as -main_tb.v or main_tb.sv. The default testbench file can be specified using -the apio.ini option ‘default-testbench’. If 'default-testbench' is not -specified and the project has exactly one testbench file, that file will be +The command 'apio sim' simulates the default or the specified testbench file \ +and displays its simulation results in a graphical GTKWave window. \ +The testbench is expected to have a name ending with _tb, such as \ +main_tb.v or main_tb.sv. The default testbench file can be specified using \ +the apio.ini option 'default-testbench'. If 'default-testbench' is not \ +specified and the project has exactly one testbench file, that file will be \ used as the default testbench. -\b -Example: - apio sim # Simulate the default testbench file. - apio sim my_module_tb.v # Simulate the specified testbench file. +Example:[code] + apio sim # Simulate the default testbench. + apio sim my_module_tb.v # Simulate the specified testbench.[/code] -[Important] Avoid using the Verilog $dumpfile() function in your testbenches, -as this may override the default name and location Apio sets for the -generated .vcd file. +[b][Important][/b] Avoid using the Verilog '$dumpfile()' function in your \ +testbenches, as this may override the default name and location Apio sets \ +for the generated .vcd file. -The sim command defines the INTERACTIVE_SIM macro, which can be used in the -testbench to distinguish between ‘apio test’ and ‘apio sim’. For example, -you can use this macro to ignore certain errors when running with ‘apio sim’ +The sim command defines the INTERACTIVE_SIM macro, which can be used in the \ +testbench to distinguish between 'apio test' and 'apio sim'. For example, \ +you can use this macro to ignore certain errors when running with 'apio sim' \ and view the erroneous signals in GTKWave. -For a sample testbench that utilizes this macro, see the example at: +For a sample testbench that utilizes this macro, see the example at: \ https://github.com/FPGAwars/apio-examples/tree/master/upduino31/testbench -[Hint] When configuring the signals in GTKWave, save the configuration so you -don’t need to repeat it each time you run the simulation. +[b][Hint][/b] When configuring the signals in GTKWave, save the \ +configuration so you don’t need to repeat it each time you run the \ +simulation. """ @click.command( name="sim", + cls=cmd_util.ApioCommand, short_help="Simulate a testbench with graphic results.", help=APIO_SIM_HELP, ) @@ -88,9 +89,7 @@ def cli( # -- we issue an error message in the scons process. testbench = apio_ctx.project.get("default-testbench", None) if testbench: - secho( - f"Using default testbench: {testbench}", fg="cyan", bold=True - ) + cout(f"Using default testbench: {testbench}", style="cyan") # -- Construct the scons sim params. sim_params = SimParams(testbench=testbench, force_sim=force) diff --git a/apio/commands/apio_system.py b/apio/commands/apio_system.py index 83307679..0a459a73 100644 --- a/apio/commands/apio_system.py +++ b/apio/commands/apio_system.py @@ -4,36 +4,41 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio system' command""" import click -from click import secho +from rich.table import Table +from rich import box +from apio.common.apio_console import cprint, PADDING, cout from apio.utils import util from apio.apio_context import ApioContext, ApioContextScope -from apio.utils.cmd_util import ApioGroup, ApioSubgroup +from apio.utils.cmd_util import ApioGroup, ApioSubgroup, ApioCommand # ------ apio system info +# -- Text in the markdown format of the python rich library. APIO_SYSTEM_INFO_HELP = """ -The command ‘apio system info’ provides general information about your system -and Apio installation, which is useful for diagnosing Apio installation issues. +The command 'apio system info' provides general information about your \ +system and Apio installation, which is useful for diagnosing Apio \ +installation issues. -\b -Examples: - apio system info # Show platform id and info. - -[Advanced] The default location of the Apio home directory, where preferences -and packages are stored, is in the .apio directory under the user’s home -directory. This location can be changed using the APIO_HOME_DIR environment -variable. +Examples:[code] + apio system info # Show general info.[/code] +[b][Advanced][/b] The default location of the Apio home directory, \ +where apio saves preferences and packages, is in the '.apio' directory \ +under the user home directory but can be changed using the system \ +environment variable 'APIO_HOME_DIR'. """ +# R0801: Similar lines in 2 files +# pylint: disable=R0801 @click.command( name="info", + cls=ApioCommand, short_help="Show platform id and other info.", help=APIO_SYSTEM_INFO_HELP, ) @@ -43,29 +48,35 @@ def _info_cli(): # Create the apio context. apio_ctx = ApioContext(scope=ApioContextScope.NO_PROJECT) - # -- Print apio version. - secho("Apio version: ", nl=False) - secho(util.get_apio_version(), fg="cyan", bold=True) - - # -- Print python version. - secho("Python version: ", nl=False) - secho(util.get_python_version(), fg="cyan", bold=True) - - # -- Print platform id. - secho("Platform id: ", nl=False) - secho(apio_ctx.platform_id, fg="cyan", bold=True) - - # -- Print apio package directory. - secho("Python package: ", nl=False) - secho(util.get_path_in_apio_package(""), fg="cyan", bold=True) + # -- Define the table. + table = Table( + show_header=True, + show_lines=True, + padding=PADDING, + box=box.SQUARE, + border_style="dim", + title="Apio System Information", + title_justify="left", + ) - # -- Print apio home directory. - secho("Apio home: ", nl=False) - secho(apio_ctx.home_dir, fg="cyan", bold=True) + table.add_column("ITEM", no_wrap=True) + table.add_column("VALUE", no_wrap=True, style="cyan") + + # -- Add rows + table.add_row("Apio version", util.get_apio_version()) + table.add_row("Python version ", util.get_python_version()) + table.add_row("Platform id ", apio_ctx.platform_id) + table.add_row("Python package ", str(util.get_path_in_apio_package(""))) + table.add_row("Apio home ", str(apio_ctx.home_dir)) + table.add_row("Apio packages ", str(apio_ctx.packages_dir)) + table.add_row( + "Veriable language server ", + str(apio_ctx.packages_dir / "verible/bin/verible-verilog-ls"), + ) - # -- Print apio home directory. - secho("Apio packages: ", nl=False) - secho(apio_ctx.packages_dir, fg="cyan", bold=True) + # -- Render the table. + cout() + cprint(table) # ------ apio system platforms @@ -94,27 +105,45 @@ def _platforms_cli(): # Create the apio context. apio_ctx = ApioContext(scope=ApioContextScope.NO_PROJECT) - # -- Print title line - secho( - f" {'[PLATFORM ID]':18} " f"{'[DESCRIPTION]'}", - fg="magenta", + # -- Define the table. + table = Table( + show_header=True, + show_lines=True, + padding=PADDING, + box=box.SQUARE, + style="dim", + title="Apio Supported Platforms", + title_justify="left", ) - # -- Print a line for each platform id. + table.add_column("PLATFORM ID", min_width=20, no_wrap=True) + table.add_column("DESCRIPTION", min_width=30, no_wrap=True) + + # -- Add rows. for platform_id, platform_info in apio_ctx.platforms.items(): - # -- Get next platform's info. description = platform_info.get("description") - # -- Determine if it's the current platform id. - fg = "green" if platform_id == apio_ctx.platform_id else None - # -- Print the line. - secho(f" {platform_id:18} {description}", fg=fg) + + # -- Mark the current platform. + if platform_id == apio_ctx.platform_id: + style = "cyan bold" + marker = "* " + else: + style = None + marker = " " + + table.add_row(f"{marker}{platform_id}", f"{description}", style=style) + + # -- Render the table. + cout() + cprint(table) # ------ apio system +# -- Text in the markdown format of the python rich library. APIO_SYSTEM_HELP = """ -The command group ‘apio system’ contains subcommands that provide information -about the system and Apio’s installation. +The command group 'apio system' contains subcommands that provide \ +information about the system and Apio’s installation. """ # -- We have only a single group with the title 'Subcommands'. diff --git a/apio/commands/apio_test.py b/apio/commands/apio_test.py index 3be43b0d..89a957bf 100644 --- a/apio/commands/apio_test.py +++ b/apio/commands/apio_test.py @@ -4,7 +4,7 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio test' command""" import sys @@ -13,37 +13,38 @@ from apio.managers.scons import SCons from apio.commands import options from apio.apio_context import ApioContext, ApioContextScope -from apio.proto.apio_pb2 import ApioTestParams +from apio.common.proto.apio_pb2 import ApioTestParams +from apio.utils import cmd_util -# --------------------------- -# -- COMMAND -# --------------------------- +# --------- apio test + +# -- Text in the markdown format of the python rich library. APIO_TEST_HELP = """ -The command ‘apio test’ simulates one or all the testbenches in the project -and is useful for automated testing of your design. Testbenches are expected -to have names ending with _tb (e.g., my_module_tb.v) and should exit with the -$fatal directive if an error is detected. +The command 'apio test' simulates one or all the testbenches in the project \ +and is useful for automated testing of your design. Testbenches are expected \ +to have names ending with _tb (e.g., my_module_tb.v) and should exit with the \ +'$fatal' directive if an error is detected. -\b -Examples +Examples:[code] apio test # Run all *_tb.v testbenches. - apio test my_module_tb.v # Run a single testbench + apio test my_module_tb.v # Run a single testbench.[/code] -[Important] Avoid using the Verilog $dumpfile() function in your testbenches, -as this may override the default name and location Apio sets for the -generated .vcd file. +[b][Important][/b] Avoid using the Verilog '$dumpfile()' function in your \ +testbenches, as this may override the default name and location Apio sets \ +for the generated .vcd file. -For a sample testbench compatible with Apio features, see: +For a sample testbench compatible with Apio features, see: \ https://github.com/FPGAwars/apio-examples/tree/master/upduino31/testbench -[Hint] To simulate a testbench with a graphical visualization of the signals, -refer to the ‘apio sim’ command. +[b][Hint][/b] To simulate a testbench with a graphical visualization \ +of the signals, refer to the 'apio sim' command. """ @click.command( name="test", + cls=cmd_util.ApioCommand, short_help="Test all or a single verilog testbench module.", help=APIO_TEST_HELP, ) diff --git a/apio/commands/apio_upgrade.py b/apio/commands/apio_upgrade.py index fa80e7e0..61adef5d 100644 --- a/apio/commands/apio_upgrade.py +++ b/apio/commands/apio_upgrade.py @@ -4,31 +4,77 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio upgrade' command""" import sys import click -from click import secho +import requests from packaging import version -from apio.utils import util +from apio.utils import util, cmd_util +from apio.common.apio_console import cout, cerror -# --------------------------- -# -- COMMAND -# --------------------------- +def get_pypi_latest_version() -> str: + """Get the latest stable version of apio from Pypi + Internet connection is required + Returns: A string with the version (Ex: "0.9.0") + Exits on an error. + """ + + # -- Read the latest apio version from pypi + # -- More information: https://warehouse.pypa.io/api-reference/json.html + try: + req = requests.get( + "https://pypi.python.org/pypi/apio/json", timeout=10 + ) + req.raise_for_status() + + # -- Connection error + except requests.exceptions.ConnectionError as e: + cout(str(e), style="yellow") + cerror("Connection error while accessing Pypi.") + sys.exit(1) + + # -- HTTP Error + except requests.exceptions.HTTPError as e: + cout(str(e), style="yellow") + cerror("HTTP error while accessing Pypi.") + sys.exit(1) + + # -- Timeout! + except requests.exceptions.Timeout as e: + cout(str(e), style="yellow") + cerror("HTTP timeout while accessing Pypi.") + sys.exit(1) + + # -- Another error + except requests.exceptions.RequestException as e: + cout(str(e), style="yellow") + cerror("HTTP exception while accessing Pypi.") + sys.exit(1) + + # -- Get the version field from the json response + ver = req.json()["info"]["version"] + + return ver + + +# ---------- apio upgrade + +# -- Text in the markdown format of the python rich library. APIO_UPGRADE_HELP = """ -The command ‘apio upgrade’ checks for the version of the latest Apio release +The command 'apio upgrade' checks for the version of the latest Apio release \ and provides upgrade directions if necessary. -\b -Examples: - apio upgrade +Examples:[code] + apio upgrade[/code] """ @click.command( name="upgrade", + cls=cmd_util.ApioCommand, short_help="Check the latest Apio version.", help=APIO_UPGRADE_HELP, ) @@ -41,38 +87,39 @@ def cli(_: click.Context): current_version = util.get_apio_version() # -- Get the latest stable version published at Pypi - latest_version = util.get_pypi_latest_version() + latest_version = get_pypi_latest_version() # -- There was an error getting the version from pypi if latest_version is None: sys.exit(1) # -- Print information about apio. - print(f"Local Apio version: {current_version}") - print(f"Lastest Apio stable version (Pypi): {latest_version}") + cout( + f"Local Apio version: {current_version}", + f"Latest Apio stable version (Pypi): {latest_version}", + style="cyan", + ) # -- Case 1: Using an old version. if version.parse(current_version) < version.parse(latest_version): - secho( - "You're not updated\nPlease execute " - "`pip install -U apio` to upgrade.", - fg="yellow", + cout( + "You're not up to date.", + "Please execute 'pip install -U apio' to upgrade.", + style="yellow", ) return # -- Case 2: Using a dev version. if version.parse(current_version) > version.parse(latest_version): - secho( - "You are using a development version! (Not stable)\n" - "Use it at your own risk", - fg="yellow", + cout( + "You are using a development version. Enjoy it at your own risk.", + style="magenta", ) return # -- Case 3: Using the latest version. - secho( + cout( f"You're up-to-date!\nApio {latest_version} is currently the " "latest stable version available.", - fg="green", - bold=True, + style="green", ) diff --git a/apio/commands/apio_upload.py b/apio/commands/apio_upload.py index c162869d..07dc1d3d 100644 --- a/apio/commands/apio_upload.py +++ b/apio/commands/apio_upload.py @@ -4,7 +4,7 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Implementation of 'apio upload' command""" import sys @@ -16,12 +16,11 @@ from apio.commands import options from apio.apio_context import ApioContext, ApioContextScope from apio.managers.programmers import construct_programmer_cmd -from apio.proto.apio_pb2 import UploadParams +from apio.common.proto.apio_pb2 import UploadParams -# --------------------------- -# -- COMMAND SPECIFIC OPTIONS -# --------------------------- +# --------- apio upload + serial_port_option = click.option( "serial_port", # Var name. "--serial-port", @@ -58,17 +57,13 @@ ) -# --------------------------- -# -- COMMAND -# --------------------------- - +# -- Text in the markdown format of the python rich library. APIO_UPLOAD_HELP = """ -The command ‘apio upload’ builds the bitstream file (similar to the apio build -command) and uploads it to the FPGA board. +The command 'apio upload' builds the bitstream file (similar to the \ +'apio build' command) and uploads it to the FPGA board. -\b -Examples: - apio upload +Examples:[code] + apio upload[/code] """ @@ -77,6 +72,7 @@ # pylint: disable=too-many-locals @click.command( name="upload", + cls=cmd_util.ApioCommand, short_help="Upload the bitstream to the FPGA.", help=APIO_UPLOAD_HELP, ) diff --git a/apio/commands/options.py b/apio/commands/options.py index cfb44a36..22a7b24c 100644 --- a/apio/commands/options.py +++ b/apio/commands/options.py @@ -4,7 +4,7 @@ # -- Authors # -- * Jesús Arroyo (2016-2019) # -- * Juan Gonzalez (obijuan) (2019-2024) -# -- Licence GPLv2 +# -- License GPLv2 """Common apio command options""" from pathlib import Path @@ -21,7 +21,7 @@ # ---------------------------------- -# -- Customizeable option generators +# -- Customizable option generators # ---------------------------------- diff --git a/apio/common/README.md b/apio/common/README.md new file mode 100644 index 00000000..d2f9c4d4 --- /dev/null +++ b/apio/common/README.md @@ -0,0 +1,2 @@ +The files in this directory are used in both the apio parent process +and the scons child process. diff --git a/apio/proto/__init__.py b/apio/common/__init__.py similarity index 100% rename from apio/proto/__init__.py rename to apio/common/__init__.py diff --git a/apio/common/apio_console.py b/apio/common/apio_console.py new file mode 100644 index 00000000..ed97a9b9 --- /dev/null +++ b/apio/common/apio_console.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +# -- This file is part of the Apio project +# -- (C) 2016-2018 FPGAwars +# -- Author Jesús Arroyo +# -- License GPLv2 +# -- Derived from: +# ---- Platformio project +# ---- (C) 2014-2016 Ivan Kravets +# ---- License Apache v2 +"""A class that manages the console output of the apio process.""" + +from io import StringIO +from dataclasses import dataclass +from typing import Optional +from rich.console import Console +from rich.ansi import AnsiDecoder +from rich.theme import Theme +from rich.text import Text + +# -- The names of the Rich library color names is available at: +# -- https://rich.readthedocs.io/en/stable/appendix/colors.html + +DOCS_TITLE = "dark_red bold" +DOCS_EMPHASIZE = "deep_sky_blue4 bold" +HELP_SUBCOMMANDS = "dark_red bold" + + +# -- Redemanded table cell padding. 1 space on the left and 3 on the right. +PADDING = padding = (0, 3, 0, 1) + +# -- Line width when rendering help and docs. +DOCS_WIDTH = 70 + + +# -- This console state is initialized at the end of this file. +@dataclass +class ConsoleState: + """Contains the state of the apio console.""" + + color_system: Optional[str] = None + force_terminal: bool = None + console: Console = None + decoder: AnsiDecoder = None + + +_state: ConsoleState = ConsoleState() + + +def configure(*, colors: bool = None, force_terminal: bool = None) -> None: + """Turn the color support on or off.""" + # -- Update color system if specified. + if colors is not None: + _state.color_system = "auto" if colors else None + + # -- Update force terminal if specified. + if force_terminal is not None: + _state.force_terminal = True if force_terminal else None + + # -- Construct the new console. The highlighting colors are optimized + # -- for 'apio docs'. + _state.console = Console( + color_system=_state.color_system, + force_terminal=_state.force_terminal, + theme=Theme( + { + "repr.str": DOCS_EMPHASIZE, + "code": DOCS_EMPHASIZE, + "repr.url": DOCS_EMPHASIZE, + "repr.number": "", + } + ), + ) + + # -- Construct the helper decoder. + _state.decoder = AnsiDecoder() + + +def console(): + """Returns the underlying console. This value should not be cached as + the console object changes when the configure() or reset() are called.""" + return _state.console + + +def reset(): + """Reset to initial configuration.""" + configure(colors=True, force_terminal=False) + + +def cunstyle(text: str) -> str: + """A replacement for click unstyle(). This function removes ansi colors + from a string.""" + text_obj: Text = _state.decoder.decode_line(text) + return text_obj.plain + + +def cout( + *text_lines: str, + style: Optional[str] = None, + nl: bool = True, +) -> None: + """Prints lines of text to the console, using the optional style.""" + + # -- If no args, just do an empty println. + if not text_lines: + text_lines = [""] + + for text_line in text_lines: + # -- User is responsible to conversion to strings. + assert isinstance(text_line, str) + + # -- If colors are off, strip potential coloring in the text. + # -- This may be coloring that we received from the scons process. + if not _state.console.color_system: + text_line = cunstyle(text_line) + + # -- Determine end of line + end = "\n" if nl else "" + + # -- Write it out using the given style. + _state.console.out(text_line, style=style, highlight=False, end=end) + + +def cerror(*text_lines: str) -> None: + """Prints one or more text lines, adding to the first one the prefix + 'Error: ' and applying to all of them the red color.""" + # -- Output the first line. + _state.console.out(f"Error: {text_lines[0]}", style="red", highlight=False) + # -- Output the rest of the lines. + for text_line in text_lines[1:]: + _state.console.out(text_line, highlight=False, style="red") + + +def cwarning(*text_lines: str) -> None: + """Prints one or more text lines, adding to the first one the prefix + 'Warning: ' and applying to all of them the yellow color.""" + # -- Emit first line. + _state.console.out( + f"Warning: {text_lines[0]}", style="yellow", highlight=False + ) + # -- Emit the rest of the lines + for text_line in text_lines[1:]: + _state.console.out(text_line, highlight=False, style="yellow") + + +def cprint( + markdown_text: str, *, style: Optional[str] = None, highlight: bool = False +) -> None: + """Render the given markdown text. Applying optional style and if enabled, + highlighting semantic elements such as strings if enabled.""" + _state.console.print( + markdown_text, + highlight=highlight, + style=style, + ) + + +class ConsoleCapture: + """A context manager to output into a string.""" + + def __init__(self): + self._saved_file = None + self._buffer = None + + def __enter__(self): + self._saved_file = _state.console.file + self._buffer = StringIO() + _state.console.file = self._buffer + return self + + def __exit__(self, exc_type, exc_value, traceback): + _state.console.file = self._saved_file + + @property + def value(self): + """Returns the captured text.""" + return self._buffer.getvalue() + + +def cstyle(text: str, style: Optional[str] = None) -> str: + """Render the text to a string using an optional style.""" + with ConsoleCapture() as capture: + _state.console.out(text, style=style, highlight=False, end="") + return capture.value + + +def docs_text( + markdown_text: str, width: int = DOCS_WIDTH, end: str = "\n" +) -> None: + """A wrapper around Console.print that is specialized for rendering + help and docs.""" + _state.console.print(markdown_text, highlight=True, width=width, end=end) + + +def docs_rule(width: int = DOCS_WIDTH): + """Print a docs horizontal separator.""" + cout("─" * width, style="dim") + + +def is_terminal(): + """Returns True if the console writes to a terminal (vs a pipe).""" + return _state.console.is_terminal + + +def cwidth(): + """Return the console width.""" + return _state.console.width + + +# -- Initialize the module. +reset() diff --git a/apio/common/apio_consts.py b/apio/common/apio_consts.py new file mode 100644 index 00000000..7733a7c7 --- /dev/null +++ b/apio/common/apio_consts.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# -- This file is part of the Apio project +# -- (C) 2016-2018 FPGAwars +# -- Author Jesús Arroyo +# -- License GPLv2 +# -- Derived from: +# ---- Platformio project +# ---- (C) 2014-2016 Ivan Kravets +# ---- License Apache v2 +"""General apio constants. Used by both the apio process (parent) and the +scons process (child)""" + +from pathlib import Path + +# -- The build directory. This is a relative path from the project directory. +BUILD_DIR = Path("_build") + +# -- Target name. This is the base file name for various build artifacts. +TARGET = str(BUILD_DIR / "hardware") diff --git a/apio/common/common_util.py b/apio/common/common_util.py new file mode 100644 index 00000000..8a224080 --- /dev/null +++ b/apio/common/common_util.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# -- This file is part of the Apio project +# -- (C) 2016-2018 FPGAwars +# -- Author Jesús Arroyo +# -- License GPLv2 +# -- Derived from: +# ---- Platformio project +# ---- (C) 2014-2016 Ivan Kravets +# ---- License Apache v2 +"""Utilities that are available for both the apio (parent) process and the +scons (child) process.""" + +import os +import debugpy + + +def maybe_wait_for_remote_debugger(env_var_name: str): + """A rendezvous point for a remote debugger. If the environment variable + of given name is set, the function will block until a remote + debugger (e.g. from Visual Studio Code) is attached. + """ + if os.getenv(env_var_name) is not None: + # NOTE: This function may be called before apio_console.py is + # initialized, so we use print() instead of cout(). + print(f"Env var '{env_var_name}' was detected.") + port = 5678 + print(f"Apio SCons for remote debugger on port localhost:{port}.") + debugpy.listen(port) + print( + "Attach Visual Studio Code python remote python debugger " + f"to port {port}.", + style="magenta", + ) + # -- Block until the debugger connects. + debugpy.wait_for_client() + # -- Here the remote debugger is attached and the program continues. + print( + "Remote debugger is attached, program continues...", + style="green", + ) diff --git a/apio/common/proto/__init__.py b/apio/common/proto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apio/proto/apio.proto b/apio/common/proto/apio.proto similarity index 78% rename from apio/proto/apio.proto rename to apio/common/proto/apio.proto index baf2f82b..37a7d691 100644 --- a/apio/proto/apio.proto +++ b/apio/common/proto/apio.proto @@ -2,19 +2,19 @@ // for passing parameters from the apio process to the scons process. // IMPORTANT: After making changes in this file, run the command 'update-proto.sh' -// to propogate them to the python stubs. Otherwise they do not take affect. +// to propagate them to the python stubs. Otherwise they do not take affect. // Online proto formatter at https://formatter.org/protobuf-formatter // NOTE: Since we use the the serialized proto data within a single invocation -// of apio, protocol buffers text mode and binary mode backeard compatibility +// of apio, protocol buffers text mode and binary mode backward compatibility // considerations do not apply. // Using proto2 for features such as 'has' and 'required'. syntax = "proto2"; -package apio.proto; +package apio.common.proto; // The supported FPGA architectures, each with its own handler. enum ApioArch { @@ -60,23 +60,24 @@ message FpgaInfo { message Verbosity { // If true, enable general verbosity. optional bool all = 1 [default = false]; - // If true, enable synthesis verbosiry. + // If true, enable synthesis verbosity. optional bool synth = 2 [default = false]; // If true, enable place-and-route verbosity. optional bool pnr = 3 [default = false]; } // Information about the environment. -message Envrionment { +message Environment { // The underlying platform id as it appears in platforms.jsonc. required string platform_id = 1; + required bool is_windows = 2; // True if apio debug is enabled. - optional bool is_debug = 2 [default = false]; + optional bool is_debug = 3 [default = false]; - // Pathes to oss-cad-suite libraries. - required string yosys_path = 3; - required string trellis_path = 4; + // Paths to oss-cad-suite libraries. + required string yosys_path = 4; + required string trellis_path = 5; } // Information about the project. @@ -115,7 +116,7 @@ message SimParams { // If not specified, scons will run if it finds a single testbench // file or exit with error if none or more than one. optional string testbench = 1 [default = ""]; - + // Force rerun of simulation, even if not change from previous // run. required bool force_sim = 2; @@ -128,7 +129,7 @@ message ApioTestParams { optional string testbench = 1 [ default = ""]; } -// Upload target specific iparams. +// Upload target specific params. message UploadParams { optional string programmer_cmd = 1; } @@ -144,10 +145,24 @@ message TargetParams { } } +// Contains parameters for working around the rich library bugs when +// using piped output on windows. +// See: +// https://github.com/Textualize/rich/issues/3082 +// https://github.com/Textualize/rich/issues/3625 +message RichLibWindowsParams { + // The encoding of stdout, e.g. 'utf-8'. + required string stdout_encoding = 1; + // Copy of WindowsConsoleFeatures.vt. + required bool vt = 2; + // Copy of WindowsConsoleFeatures.truecolor. + required bool truecolor = 3; +} + // The top level messages that is passed from the apio process to // the scons process. message SconsParams { - // An arbitrary timesamp string that we pass also as an scons argument + // An arbitrary timestamp string that we pass also as an scons argument // to verify that scons reads the correct versions of the params file. required string timestamp = 1; @@ -162,11 +177,14 @@ message SconsParams { optional Verbosity verbosity = 4; // General information about the environment. - required Envrionment envrionment = 5; + required Environment environment = 5; // General information about the project required Project project = 6; // Additional params for for scons targets that need it.. optional TargetParams target = 7; + + // Should be populated if an only if environment.is_windows is true. + optional RichLibWindowsParams rich_lib_windows_params = 8; } diff --git a/apio/common/proto/apio_pb2.py b/apio/common/proto/apio_pb2.py new file mode 100644 index 00000000..86de5530 --- /dev/null +++ b/apio/common/proto/apio_pb2.py @@ -0,0 +1,74 @@ + +# pylint: disable=C0114, C0115, C0301, C0303, C0411 +# pylint: disable=E0245, E0602, E1139 +# pylint: disable=R0913, R0801, R0917 +# pylint: disable=W0212, W0223, W0311, W0613, W0622 + +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: apio.proto +# Protobuf Python Version: 5.29.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 0, + '', + 'apio.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\napio.proto\x12\x11\x61pio.common.proto\"+\n\rIce40FpgaInfo\x12\x0c\n\x04type\x18\x01 \x02(\t\x12\x0c\n\x04pack\x18\x02 \x02(\t\"9\n\x0c\x45\x63p5FpgaInfo\x12\x0c\n\x04type\x18\x04 \x02(\t\x12\x0c\n\x04pack\x18\x05 \x02(\t\x12\r\n\x05speed\x18\x06 \x02(\t\"\x1f\n\rGowinFpgaInfo\x12\x0e\n\x06\x66\x61mily\x18\x04 \x02(\t\"\xda\x01\n\x08\x46pgaInfo\x12\x0f\n\x07\x66pga_id\x18\x01 \x02(\t\x12\x10\n\x08part_num\x18\x02 \x02(\t\x12\x0c\n\x04size\x18\x03 \x02(\t\x12\x31\n\x05ice40\x18\n \x01(\x0b\x32 .apio.common.proto.Ice40FpgaInfoH\x00\x12/\n\x04\x65\x63p5\x18\x0b \x01(\x0b\x32\x1f.apio.common.proto.Ecp5FpgaInfoH\x00\x12\x31\n\x05gowin\x18\x0c \x01(\x0b\x32 .apio.common.proto.GowinFpgaInfoH\x00\x42\x06\n\x04\x61rch\"I\n\tVerbosity\x12\x12\n\x03\x61ll\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x14\n\x05synth\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x12\n\x03pnr\x18\x03 \x01(\x08:\x05\x66\x61lse\"y\n\x0b\x45nvironment\x12\x13\n\x0bplatform_id\x18\x01 \x02(\t\x12\x12\n\nis_windows\x18\x02 \x02(\x08\x12\x17\n\x08is_debug\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x12\n\nyosys_path\x18\x04 \x02(\t\x12\x14\n\x0ctrellis_path\x18\x05 \x02(\t\"T\n\x07Project\x12\x10\n\x08\x62oard_id\x18\x01 \x02(\t\x12\x12\n\ntop_module\x18\x02 \x02(\t\x12#\n\x19yosys_synth_extra_options\x18\x03 \x01(\t:\x00\"\x98\x01\n\nLintParams\x12\x14\n\ntop_module\x18\x01 \x01(\t:\x00\x12\x1c\n\rverilator_all\x18\x02 \x01(\x08:\x05\x66\x61lse\x12!\n\x12verilator_no_style\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x12verilator_no_warns\x18\x04 \x03(\t\x12\x17\n\x0fverilator_warns\x18\x05 \x03(\t\"Z\n\x0bGraphParams\x12\x37\n\x0boutput_type\x18\x01 \x02(\x0e\x32\".apio.common.proto.GraphOutputType\x12\x12\n\ntop_module\x18\x02 \x01(\t\"3\n\tSimParams\x12\x13\n\ttestbench\x18\x01 \x01(\t:\x00\x12\x11\n\tforce_sim\x18\x02 \x02(\x08\"%\n\x0e\x41pioTestParams\x12\x13\n\ttestbench\x18\x01 \x01(\t:\x00\"&\n\x0cUploadParams\x12\x16\n\x0eprogrammer_cmd\x18\x01 \x01(\t\"\x8b\x02\n\x0cTargetParams\x12-\n\x04lint\x18\n \x01(\x0b\x32\x1d.apio.common.proto.LintParamsH\x00\x12/\n\x05graph\x18\x0b \x01(\x0b\x32\x1e.apio.common.proto.GraphParamsH\x00\x12+\n\x03sim\x18\x0c \x01(\x0b\x32\x1c.apio.common.proto.SimParamsH\x00\x12\x31\n\x04test\x18\r \x01(\x0b\x32!.apio.common.proto.ApioTestParamsH\x00\x12\x31\n\x06upload\x18\x0e \x01(\x0b\x32\x1f.apio.common.proto.UploadParamsH\x00\x42\x08\n\x06target\"N\n\x14RichLibWindowsParams\x12\x17\n\x0fstdout_encoding\x18\x01 \x02(\t\x12\n\n\x02vt\x18\x02 \x02(\x08\x12\x11\n\ttruecolor\x18\x03 \x02(\x08\"\x89\x03\n\x0bSconsParams\x12\x11\n\ttimestamp\x18\x01 \x02(\t\x12)\n\x04\x61rch\x18\x02 \x02(\x0e\x32\x1b.apio.common.proto.ApioArch\x12.\n\tfpga_info\x18\x03 \x02(\x0b\x32\x1b.apio.common.proto.FpgaInfo\x12/\n\tverbosity\x18\x04 \x01(\x0b\x32\x1c.apio.common.proto.Verbosity\x12\x33\n\x0b\x65nvironment\x18\x05 \x02(\x0b\x32\x1e.apio.common.proto.Environment\x12+\n\x07project\x18\x06 \x02(\x0b\x32\x1a.apio.common.proto.Project\x12/\n\x06target\x18\x07 \x01(\x0b\x32\x1f.apio.common.proto.TargetParams\x12H\n\x17rich_lib_windows_params\x18\x08 \x01(\x0b\x32\'.apio.common.proto.RichLibWindowsParams*@\n\x08\x41pioArch\x12\x14\n\x10\x41RCH_UNSPECIFIED\x10\x00\x12\t\n\x05ICE40\x10\x01\x12\x08\n\x04\x45\x43P5\x10\x02\x12\t\n\x05GOWIN\x10\x03*B\n\x0fGraphOutputType\x12\x14\n\x10TYPE_UNSPECIFIED\x10\x00\x12\x07\n\x03SVG\x10\x01\x12\x07\n\x03PNG\x10\x02\x12\x07\n\x03PDF\x10\x03') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'apio_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_APIOARCH']._serialized_start=1800 + _globals['_APIOARCH']._serialized_end=1864 + _globals['_GRAPHOUTPUTTYPE']._serialized_start=1866 + _globals['_GRAPHOUTPUTTYPE']._serialized_end=1932 + _globals['_ICE40FPGAINFO']._serialized_start=33 + _globals['_ICE40FPGAINFO']._serialized_end=76 + _globals['_ECP5FPGAINFO']._serialized_start=78 + _globals['_ECP5FPGAINFO']._serialized_end=135 + _globals['_GOWINFPGAINFO']._serialized_start=137 + _globals['_GOWINFPGAINFO']._serialized_end=168 + _globals['_FPGAINFO']._serialized_start=171 + _globals['_FPGAINFO']._serialized_end=389 + _globals['_VERBOSITY']._serialized_start=391 + _globals['_VERBOSITY']._serialized_end=464 + _globals['_ENVIRONMENT']._serialized_start=466 + _globals['_ENVIRONMENT']._serialized_end=587 + _globals['_PROJECT']._serialized_start=589 + _globals['_PROJECT']._serialized_end=673 + _globals['_LINTPARAMS']._serialized_start=676 + _globals['_LINTPARAMS']._serialized_end=828 + _globals['_GRAPHPARAMS']._serialized_start=830 + _globals['_GRAPHPARAMS']._serialized_end=920 + _globals['_SIMPARAMS']._serialized_start=922 + _globals['_SIMPARAMS']._serialized_end=973 + _globals['_APIOTESTPARAMS']._serialized_start=975 + _globals['_APIOTESTPARAMS']._serialized_end=1012 + _globals['_UPLOADPARAMS']._serialized_start=1014 + _globals['_UPLOADPARAMS']._serialized_end=1052 + _globals['_TARGETPARAMS']._serialized_start=1055 + _globals['_TARGETPARAMS']._serialized_end=1322 + _globals['_RICHLIBWINDOWSPARAMS']._serialized_start=1324 + _globals['_RICHLIBWINDOWSPARAMS']._serialized_end=1402 + _globals['_SCONSPARAMS']._serialized_start=1405 + _globals['_SCONSPARAMS']._serialized_end=1798 +# @@protoc_insertion_point(module_scope) diff --git a/apio/proto/apio_pb2.pyi b/apio/common/proto/apio_pb2.pyi similarity index 86% rename from apio/proto/apio_pb2.pyi rename to apio/common/proto/apio_pb2.pyi index 7ee9ee86..d44e4ef7 100644 --- a/apio/proto/apio_pb2.pyi +++ b/apio/common/proto/apio_pb2.pyi @@ -84,17 +84,19 @@ class Verbosity(_message.Message): pnr: bool def __init__(self, all: bool = ..., synth: bool = ..., pnr: bool = ...) -> None: ... -class Envrionment(_message.Message): - __slots__ = ("platform_id", "is_debug", "yosys_path", "trellis_path") +class Environment(_message.Message): + __slots__ = ("platform_id", "is_windows", "is_debug", "yosys_path", "trellis_path") PLATFORM_ID_FIELD_NUMBER: _ClassVar[int] + IS_WINDOWS_FIELD_NUMBER: _ClassVar[int] IS_DEBUG_FIELD_NUMBER: _ClassVar[int] YOSYS_PATH_FIELD_NUMBER: _ClassVar[int] TRELLIS_PATH_FIELD_NUMBER: _ClassVar[int] platform_id: str + is_windows: bool is_debug: bool yosys_path: str trellis_path: str - def __init__(self, platform_id: _Optional[str] = ..., is_debug: bool = ..., yosys_path: _Optional[str] = ..., trellis_path: _Optional[str] = ...) -> None: ... + def __init__(self, platform_id: _Optional[str] = ..., is_windows: bool = ..., is_debug: bool = ..., yosys_path: _Optional[str] = ..., trellis_path: _Optional[str] = ...) -> None: ... class Project(_message.Message): __slots__ = ("board_id", "top_module", "yosys_synth_extra_options") @@ -162,20 +164,32 @@ class TargetParams(_message.Message): upload: UploadParams def __init__(self, lint: _Optional[_Union[LintParams, _Mapping]] = ..., graph: _Optional[_Union[GraphParams, _Mapping]] = ..., sim: _Optional[_Union[SimParams, _Mapping]] = ..., test: _Optional[_Union[ApioTestParams, _Mapping]] = ..., upload: _Optional[_Union[UploadParams, _Mapping]] = ...) -> None: ... +class RichLibWindowsParams(_message.Message): + __slots__ = ("stdout_encoding", "vt", "truecolor") + STDOUT_ENCODING_FIELD_NUMBER: _ClassVar[int] + VT_FIELD_NUMBER: _ClassVar[int] + TRUECOLOR_FIELD_NUMBER: _ClassVar[int] + stdout_encoding: str + vt: bool + truecolor: bool + def __init__(self, stdout_encoding: _Optional[str] = ..., vt: bool = ..., truecolor: bool = ...) -> None: ... + class SconsParams(_message.Message): - __slots__ = ("timestamp", "arch", "fpga_info", "verbosity", "envrionment", "project", "target") + __slots__ = ("timestamp", "arch", "fpga_info", "verbosity", "environment", "project", "target", "rich_lib_windows_params") TIMESTAMP_FIELD_NUMBER: _ClassVar[int] ARCH_FIELD_NUMBER: _ClassVar[int] FPGA_INFO_FIELD_NUMBER: _ClassVar[int] VERBOSITY_FIELD_NUMBER: _ClassVar[int] - ENVRIONMENT_FIELD_NUMBER: _ClassVar[int] + ENVIRONMENT_FIELD_NUMBER: _ClassVar[int] PROJECT_FIELD_NUMBER: _ClassVar[int] TARGET_FIELD_NUMBER: _ClassVar[int] + RICH_LIB_WINDOWS_PARAMS_FIELD_NUMBER: _ClassVar[int] timestamp: str arch: ApioArch fpga_info: FpgaInfo verbosity: Verbosity - envrionment: Envrionment + environment: Environment project: Project target: TargetParams - def __init__(self, timestamp: _Optional[str] = ..., arch: _Optional[_Union[ApioArch, str]] = ..., fpga_info: _Optional[_Union[FpgaInfo, _Mapping]] = ..., verbosity: _Optional[_Union[Verbosity, _Mapping]] = ..., envrionment: _Optional[_Union[Envrionment, _Mapping]] = ..., project: _Optional[_Union[Project, _Mapping]] = ..., target: _Optional[_Union[TargetParams, _Mapping]] = ...) -> None: ... + rich_lib_windows_params: RichLibWindowsParams + def __init__(self, timestamp: _Optional[str] = ..., arch: _Optional[_Union[ApioArch, str]] = ..., fpga_info: _Optional[_Union[FpgaInfo, _Mapping]] = ..., verbosity: _Optional[_Union[Verbosity, _Mapping]] = ..., environment: _Optional[_Union[Environment, _Mapping]] = ..., project: _Optional[_Union[Project, _Mapping]] = ..., target: _Optional[_Union[TargetParams, _Mapping]] = ..., rich_lib_windows_params: _Optional[_Union[RichLibWindowsParams, _Mapping]] = ...) -> None: ... diff --git a/apio/proto/update-protos.sh b/apio/common/proto/update-protos.sh similarity index 100% rename from apio/proto/update-protos.sh rename to apio/common/proto/update-protos.sh diff --git a/apio/common/rich_lib_windows.py b/apio/common/rich_lib_windows.py new file mode 100644 index 00000000..97f5667c --- /dev/null +++ b/apio/common/rich_lib_windows.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# -- This file is part of the Apio project +# -- (C) 2016-2018 FPGAwars +# -- Author Jesús Arroyo +# -- License GPLv2 +# -- Derived from: +# ---- Platformio project +# ---- (C) 2014-2016 Ivan Kravets +# ---- License Apache v2 +"""Functions to workaround the rich library bugs when stdout is piped out +on windows.""" + +import sys +import rich.console +from apio.common.proto.apio_pb2 import RichLibWindowsParams + +# For accessing rich.console._windows_console_features +# pylint: disable=protected-access + + +def get_workaround_params() -> RichLibWindowsParams: + """Called on the apio (parent) process side, when running on windows, + to collect the parameters for the rich library workaround.""" + result = RichLibWindowsParams( + stdout_encoding=sys.stdout.encoding, + vt=rich.console._windows_console_features.vt, + truecolor=rich.console._windows_console_features.truecolor, + ) + assert result.IsInitialized(), result + return result + + +def apply_workaround(params: RichLibWindowsParams): + """Called on the scons (child) process side, when running on windows, + to apply the the workaround for the rich library.""" + assert params.IsInitialized, params + + # This takes care of the table graphic box. + # https://github.com/Textualize/rich/issues/3625 + sys.stdout.reconfigure(encoding=params.stdout_encoding) + + # This enables the colors. + # https://github.com/Textualize/rich/issues/3082 + assert rich.console._windows_console_features is not None + rich.console._windows_console_features.vt = params.vt + rich.console._windows_console_features.truecolor = params.truecolor diff --git a/apio/managers/downloader.py b/apio/managers/downloader.py index 360ada42..156f808d 100644 --- a/apio/managers/downloader.py +++ b/apio/managers/downloader.py @@ -2,13 +2,13 @@ # -- This file is part of the Apio project # -- (C) 2016-2019 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """Implement a remote file downloader. Used to fetch packages from github -packages release repositorie.s +packages release repositories. """ # pylint: disable=fixme @@ -17,11 +17,11 @@ from math import ceil import requests -import click -from click import secho +from rich.progress import track from apio.utils import util +from apio.common.apio_console import cout, console -# -- Timeout for geting a reponse from the server when downloading +# -- Timeout for getting a response from the server when downloading # -- a file (in seconds) TIMEOUT_SECS = 10 @@ -54,19 +54,16 @@ def __init__(self, url: str, dest_dir=None): # -- Add the path self.destination = dest_dir / self.fname - self._progressbar = None - self._request = None - # -- Request the file self._request = requests.get(url, stream=True, timeout=TIMEOUT_SECS) # -- Raise an exception in case of download error... if self._request.status_code != 200: - secho( + cout( "Got an unexpected HTTP status code: " - f"{self._request.status_code}" - f"\nWhen downloading {url}", - fg="red", + f"{self._request.status_code}", + f"When downloading {url}", + style="red", ) raise util.ApioException() @@ -85,19 +82,20 @@ def start(self): with open(self.destination, "wb") as file: # -- Get the file length in Kbytes - chunks = int(ceil(self.get_size() / float(self.CHUNK_SIZE))) - - # -- Download the file. Show a progress bar - with click.progressbar( - length=chunks, - label=click.style("Downloading", fg="yellow"), - fill_char=click.style("█", fg="cyan", bold=True), - empty_char=click.style("░", fg="cyan", bold=True), - ) as pbar: - for _ in pbar: - - # -- Receive next block of bytes - file.write(next(itercontent)) + num_chunks = int(ceil(self.get_size() / float(self.CHUNK_SIZE))) + + # -- Download and write the chunks, while displaying the progress. + for _ in track( + range(num_chunks), + description="Downloading", + console=console(), + ): + + file.write(next(itercontent)) + + # -- Check that the iterator reached its end. When the end is + # -- reached, next() returns the default value None. + assert next(itercontent, None) is None # -- Download done! self._request.close() diff --git a/apio/managers/drivers.py b/apio/managers/drivers.py index f8f99096..e9f5cfa2 100644 --- a/apio/managers/drivers.py +++ b/apio/managers/drivers.py @@ -2,7 +2,7 @@ # -- This file is part of the Apio project # -- (C) 2016-2019 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 """Manage board drivers""" @@ -10,8 +10,8 @@ import shutil import subprocess from pathlib import Path -from click import secho from apio.utils import util +from apio.common.apio_console import cout, cerror from apio.apio_context import ApioContext from apio.managers import installer @@ -41,7 +41,7 @@ 8. Disconnect and reconnect your FPGA board for the new driver to take affect. - 9. Run the command `apio system lsftdi` and verify that + 9. Run the command 'apio system lsftdi' and verify that your board is listed. """ @@ -145,32 +145,32 @@ def ftdi_install(self) -> int: Returns a process exit code. """ - if self.apio_ctx.is_linux(): + if self.apio_ctx.is_linux: return self._ftdi_install_linux() - if self.apio_ctx.is_darwin(): + if self.apio_ctx.is_darwin: return self._ftdi_install_darwin() - if self.apio_ctx.is_windows(): + if self.apio_ctx.is_windows: return self._ftdi_install_windows() - secho(f"Error: unknown platform type '{self.apio_ctx.platform_id}'.") + cerror(f"Unknown platform type '{self.apio_ctx.platform_id}'.") return 1 def ftdi_uninstall(self) -> int: """Uninstalls the FTDI driver. Function is platform dependent. Returns a process exit code. """ - if self.apio_ctx.is_linux(): + if self.apio_ctx.is_linux: return self._ftdi_uninstall_linux() - if self.apio_ctx.is_darwin(): + if self.apio_ctx.is_darwin: return self._ftdi_uninstall_darwin() - if self.apio_ctx.is_windows(): + if self.apio_ctx.is_windows: return self._ftdi_uninstall_windows() - secho(f"Error: unknown platform '{self.apio_ctx.platform_id}'.") + cerror(f"Unknown platform '{self.apio_ctx.platform_id}'.") return 1 def serial_install(self) -> int: @@ -178,53 +178,53 @@ def serial_install(self) -> int: Returns a process exit code. """ - if self.apio_ctx.is_linux(): + if self.apio_ctx.is_linux: return self._serial_install_linux() - if self.apio_ctx.is_darwin(): + if self.apio_ctx.is_darwin: return self._serial_install_darwin() - if self.apio_ctx.is_windows(): + if self.apio_ctx.is_windows: return self._serial_install_windows() - secho(f"Error: unknown platform '{self.apio_ctx.platform_id}'.") + cerror(f"Unknown platform '{self.apio_ctx.platform_id}'.") return 1 def serial_uninstall(self) -> int: """Uninstalls the serial driver. Function is platform dependent. Returns a process exit code. """ - if self.apio_ctx.is_linux(): + if self.apio_ctx.is_linux: return self._serial_uninstall_linux() - if self.apio_ctx.is_darwin(): + if self.apio_ctx.is_darwin: return self._serial_uninstall_darwin() - if self.apio_ctx.is_windows(): + if self.apio_ctx.is_windows: return self._serial_uninstall_windows() - secho(f"Error: unknown platform '{self.apio_ctx.platform_id}'.") + cerror(f"Unknown platform '{self.apio_ctx.platform_id}'.") return 1 def pre_upload(self): """Operations to do before uploading a design Only for mac platforms""" - if self.apio_ctx.is_darwin(): + if self.apio_ctx.is_darwin: self._pre_upload_darwin() def post_upload(self): """Operations to do after uploading a design Only for mac platforms""" - if self.apio_ctx.is_darwin(): + if self.apio_ctx.is_darwin: self._post_upload_darwin() def _ftdi_install_linux(self) -> int: """Drivers install on Linux. It copies the .rules file into the corresponding folder. Return process exit code.""" - secho("Configure FTDI drivers for FPGA") + cout("Configure FTDI drivers for FPGA") # -- Check if the target rules file already exists if not self.ftdi_rules_system_path.exists(): @@ -243,10 +243,10 @@ def _ftdi_install_linux(self) -> int: # -- Execute the commands for reloading the udev system self._reload_rules_linux() - secho("FTDI drivers installed", fg="green", bold=True) - secho("Unplug and reconnect your board", fg="yellow") + cout("FTDI drivers installed", style="green") + cout("Unplug and reconnect your board", style="yellow") else: - secho("Already installed", fg="yellow") + cout("Already installed", style="yellow") return 0 @@ -258,7 +258,7 @@ def _ftdi_uninstall_linux(self): # -- Remove the .rules file, if it exists if self.ftdi_rules_system_path.exists(): - secho("Revert FTDI drivers configuration") + cout("Revert FTDI drivers configuration") # -- Execute the sudo rm rules_file command subprocess.call(["sudo", "rm", str(self.ftdi_rules_system_path)]) @@ -266,17 +266,17 @@ def _ftdi_uninstall_linux(self): # -- # -- Execute the commands for reloading the udev system self._reload_rules_linux() - secho("FTDI drivers uninstalled", fg="green", bold=True) - secho("Unplug and reconnect your board", fg="yellow") + cout("FTDI drivers uninstalled", style="green") + cout("Unplug and reconnect your board", style="yellow") else: - secho("Already uninstalled", fg="yellow") + cout("Already uninstalled", style="yellow") return 0 def _serial_install_linux(self): """Serial drivers install on Linux. Returns process exit code.""" - secho("Configure Serial drivers for FPGA") + cout("Configure Serial drivers for FPGA") # -- Check if the target rules file already exists if not self.serial_rules_system_path.exists(): @@ -298,15 +298,15 @@ def _serial_install_linux(self): # -- Execute the commands for reloading the udev system self._reload_rules_linux() - secho("Serial drivers installed", fg="green", bold=True) - secho("Unplug and reconnect your board", fg="yellow") + cout("Serial drivers installed", style="green") + cout("Unplug and reconnect your board", style="yellow") if group_added: - secho( + cout( "Restart your machine to install the dialout group", - fg="yellow", + style="yellow", ) else: - secho("Already installed", fg="yellow") + cout("Already installed", style="yellow") return 0 @@ -316,17 +316,17 @@ def _serial_uninstall_linux(self) -> int: # -- For disabling the serial driver the corresponding .rules file # -- should be removed, it it exists if self.serial_rules_system_path.exists(): - secho("Revert Serial drivers configuration") + cout("Revert Serial drivers configuration") # -- Execute the sudo rm rule_file cmd subprocess.call(["sudo", "rm", str(self.serial_rules_system_path)]) # -- Execute the commands for reloading the udev system self._reload_rules_linux() - secho("Serial drivers uninstalled", fg="green", bold=True) - secho("Unplug and reconnect your board", fg="yellow") + cout("Serial drivers uninstalled", style="green") + cout("Unplug and reconnect your board", style="yellow") else: - secho("Already uninstalled", fg="yellow") + cout("Already uninstalled", style="yellow") return 0 @@ -359,22 +359,22 @@ def _ftdi_install_darwin(self) -> int: # Check homebrew brew = subprocess.call("which brew > /dev/null", shell=True) if brew != 0: - secho("Error: homebrew is required", fg="red") + cerror("Homebrew is required") return 1 - secho("Install FTDI drivers for FPGA") + cout("Install FTDI drivers for FPGA") subprocess.call(["brew", "update"]) self._brew_install_darwin("libffi") self._brew_install_darwin("libftdi") self.apio_ctx.profile.add_setting("macos_ftdi_drivers", True) - secho("FTDI drivers installed", fg="green", bold=True) + cout("FTDI drivers installed", style="green") return 0 def _ftdi_uninstall_darwin(self): """Uninstalls FTDI driver on darwin. Returns process status code.""" - secho("Uninstall FTDI drivers configuration") + cout("Uninstall FTDI drivers configuration") self.apio_ctx.profile.add_setting("macos_ftdi_drivers", False) - secho("FTDI drivers uninstalled", fg="green", bold=True) + cout("FTDI drivers uninstalled", style="green") return 0 def _serial_install_darwin(self): @@ -382,21 +382,21 @@ def _serial_install_darwin(self): # Check homebrew brew = subprocess.call("which brew > /dev/null", shell=True) if brew != 0: - secho("Error: homebrew is required", fg="red") + cerror("Homebrew is required") return 1 - secho("Install Serial drivers for FPGA") + cout("Install Serial drivers for FPGA") subprocess.call(["brew", "update"]) self._brew_install_darwin("libffi") self._brew_install_darwin("libusb") # self._brew_install_serial_drivers_darwin() - secho("Serial drivers installed", fg="green", bold=True) + cout("Serial drivers installed", style="green") return 0 def _serial_uninstall_darwin(self): """Uninstalls serial driver on darwin. Returns process status code.""" - secho("Uninstall Serial drivers configuration") - secho("Serial drivers uninstalled", fg="green", bold=True) + cout("Uninstall Serial drivers configuration") + cout("Serial drivers uninstalled", style="green") return 0 def _brew_install_darwin(self, brew_package): @@ -460,19 +460,17 @@ def _ftdi_install_windows(self) -> int: zadig_exe = drivers_base_dir / "bin" / "zadig.exe" # -- Show messages for the user - secho( - "\nStarting the interactive config tool zadig.exe.", - fg="green", - bold=True, + cout( + "\nStarting the interactive config tool zadig.exe.", style="green" ) - secho(FTDI_INSTALL_INSTRUCTIONS_WINDOWS, fg="yellow") + cout(FTDI_INSTALL_INSTRUCTIONS_WINDOWS, style="yellow") # -- Execute zadig! # -- We execute it using os.system() rather than by # -- util.exec_command() because zadig required permissions # -- elevation. exit_code = os.system(str(zadig_exe)) - secho("FTDI drivers configuration finished", fg="green", bold=True) + cout("FTDI drivers configuration finished", style="green") # -- Remove zadig.ini from the current folder. It is no longer # -- needed @@ -485,10 +483,8 @@ def _ftdi_uninstall_windows(self) -> int: # -- Check that the required packages exist. installer.install_missing_packages_on_the_fly(self.apio_ctx) - secho( - "\nStarting the interactive Device Manager.", fg="green", bold=True - ) - secho(FTDI_UNINSTALL_INSTRUCTIONS_WINDOWS, fg="yellow") + cout("\nStarting the interactive Device Manager.", style="green") + cout(FTDI_UNINSTALL_INSTRUCTIONS_WINDOWS, style="yellow") # -- We launch the device manager using os.system() rather than with # -- util.exec_command() because util.exec_command() does not support @@ -505,12 +501,8 @@ def _serial_install_windows(self) -> int: drivers_base_dir = self.apio_ctx.get_package_dir("drivers") drivers_bin_dir = drivers_base_dir / "bin" - secho( - "\nStarting the interactive Serial Installer.", - fg="green", - bold=True, - ) - secho(SERIAL_INSTALL_INSTRUCTIONS_WINDOWS, fg="yellow") + cout("\nStarting the interactive Serial Installer.", style="green") + cout(SERIAL_INSTALL_INSTRUCTIONS_WINDOWS, style="yellow") # -- We launch the device manager using os.system() rather than with # -- util.exec_command() because util.exec_command() does not support @@ -525,10 +517,8 @@ def _serial_uninstall_windows(self) -> int: # -- Check that the required packages exist. installer.install_missing_packages_on_the_fly(self.apio_ctx) - secho( - "\nStarting the interactive Device Manager.", fg="green", bold=True - ) - secho(SERIAL_UNINSTALL_INSTRUCTIONS_WINDOWS, fg="yellow") + cout("\nStarting the interactive Device Manager.", style="green") + cout(SERIAL_UNINSTALL_INSTRUCTIONS_WINDOWS, style="yellow") # -- We launch the device manager using os.system() rather than with # -- util.exec_command() because util.exec_command() does not support diff --git a/apio/managers/examples.py b/apio/managers/examples.py index 6c7f879d..18e03628 100644 --- a/apio/managers/examples.py +++ b/apio/managers/examples.py @@ -4,7 +4,7 @@ # -- This file is part of the Apio project # -- (C) 2016-2019 FPGAwars # -- Author Jesús Arroyo, Juan González -# -- Licence GPLv2 +# -- License GPLv2 import shutil import sys @@ -12,7 +12,7 @@ from pathlib import Path, PosixPath from dataclasses import dataclass from typing import Optional, List, Dict -from click import secho, style, echo +from apio.common.apio_console import cout, cstyle, cerror from apio.apio_context import ApioContext from apio.managers import installer @@ -49,11 +49,11 @@ def __init__(self, apio_ctx: ApioContext): def is_dir_empty(self, path: Path) -> bool: """Return true if the given dir is empty, ignoring hidden entry. That is, the dir may contain only hidden entries. - We use this relaxed criteria of emptyness to avoid user confusion. - We could use glop.glob() but in python 3.10 and eariler it doesn't + We use this relaxed criteria of emptiness to avoid user confusion. + We could use glop.glob() but in python 3.10 and earlier it doesn't have the 'include_hidden' argument. """ - # -- Check prerequirement. + # -- Check prerequisites. assert path.is_dir(), f"Not a dir: {path}" # -- Iterate directory entries @@ -118,7 +118,7 @@ def get_examples_infos(self) -> List[ExampleInfo]: ) examples.append(example_info) - # -- Sort in-place by ascceding example name, case insensitive. + # -- Sort in-place by acceding example name, case insensitive. examples.sort(key=lambda x: x.name.lower()) return examples @@ -168,11 +168,11 @@ def copy_example_files(self, example_name: str, dst_dir_path: Path): example_info: ExampleInfo = self.lookup_example_info(example_name) if not example_info: - secho(f"Error: example '{example_name}' not found.", fg="red") - secho( - "Run 'apio example list' for the list of examples.\n" + cerror(f"Example '{example_name}' not found.") + cout( + "Run 'apio example list' for the list of examples.", "Expecting an example name like alhambra-ii/ledon.", - fg="yellow", + style="yellow", ) sys.exit(1) @@ -183,29 +183,26 @@ def copy_example_files(self, example_name: str, dst_dir_path: Path): # -- we ignore hidden files and directory. if dst_dir_path.is_dir(): if not self.is_dir_empty(dst_dir_path): - secho( - f"Error: destination directory '{str(dst_dir_path)}' " - "is not empty.", - fg="red", + cerror( + f"Destination directory '{str(dst_dir_path)}' " + "is not empty." ) sys.exit(1) else: dst_dir_path.mkdir(parents=True, exist_ok=False) - secho("Copying " + example_name + " example files.") + cout("Copying " + example_name + " example files.") # -- Go though all the files in the example folder. for file in src_example_path.iterdir(): # -- Copy the file unless it's 'info' which we ignore. if file.name != "info": shutil.copy(file, dst_dir_path) - styled_name = style( - os.path.basename(file), fg="cyan", bold=True - ) - echo(f"Fetched file {styled_name}") + styled_name = cstyle(os.path.basename(file), style="cyan") + cout(f"Fetched file {styled_name}") # -- Inform the user. - secho("Example fetched successfully.", fg="green", bold=True) + cout("Example fetched successfully.", style="green") def get_board_examples(self, board_name) -> List[ExampleInfo]: """Returns the list of examples with given board name.""" @@ -232,13 +229,13 @@ def copy_board_examples(self, board_name: str, dst_dir: Path): # dst_dir = util.resolve_project_dir( # dst_dir, create_if_missing=True # ) - board_exaamples = self.get_board_examples(board_name) - if not board_exaamples: - secho(f"Error: no examples for board '{board_name}.", fg="red") - secho( - "Run 'apio examples list' for the list of examples.\n" + board_examples = self.get_board_examples(board_name) + if not board_examples: + cerror(f"No examples for board '{board_name}.") + cout( + "Run 'apio examples list' for the list of examples.", "Expecting a board name such as 'alhambra-ii.", - fg="yellow", + style="yellow", ) sys.exit(1) @@ -250,37 +247,32 @@ def copy_board_examples(self, board_name: str, dst_dir: Path): # -- If the source example path is not a folder... it is an error if not src_board_dir.is_dir(): - secho( - f"Error: examples for board [{board_name}] not found.", - fg="red", - ) - secho( - "Expecting a board name such as 'alhambra-ii'.\n" - "Run 'apio examples list' for the list of available examples.", - fg="yellow", + cerror(f"Examples for board [{board_name}] not found.") + cout( + "Run 'apio examples list' for the list of available " + "examples.", + "Expecting a board name such as 'alhambra-ii'.", + style="yellow", ) sys.exit(1) if dst_board_dir.is_dir(): # -- To avoid confusion to the user, we ignore hidden files. if not self.is_dir_empty(dst_board_dir): - secho( - f"Error: destination directory '{str(dst_board_dir)}' " - "is not empty.", - fg="red", + cerror( + f"Destination directory '{str(dst_board_dir)}' " + "is not empty." ) sys.exit(1) else: - secho(f"Creating directory {dst_board_dir}.") + cout(f"Creating directory {dst_board_dir}.") dst_board_dir.mkdir(parents=True, exist_ok=False) # -- Copy the directory tree. shutil.copytree(src_board_dir, dst_board_dir, dirs_exist_ok=True) for example_name in os.listdir(dst_board_dir): - styled_name = style( - f"{board_name}/{example_name}", fg="cyan", bold=True - ) - echo(f"Fetched example {styled_name}") + styled_name = cstyle(f"{board_name}/{example_name}", style="cyan") + cout(f"Fetched example {styled_name}") - secho("Board examples fetched successfully.", fg="green", bold=True) + cout("Board examples fetched successfully.", style="green") diff --git a/apio/managers/installer.py b/apio/managers/installer.py index 3a5e8bd8..924689e4 100644 --- a/apio/managers/installer.py +++ b/apio/managers/installer.py @@ -2,7 +2,7 @@ # -- This file is part of the Apio project # -- (C) 2016-2021 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 """Package install/uninstall functionality. Used by the 'apio packages' command. """ @@ -11,7 +11,7 @@ from pathlib import Path from typing import Tuple import shutil -from click import secho +from apio.common.apio_console import cout, cerror from apio.apio_context import ApioContext from apio.managers.downloader import FileDownloader from apio.managers.unpacker import FileUnpacker @@ -102,16 +102,16 @@ def _download_package_file(url: str, dir_path: Path) -> str: filepath.unlink() # -- Inform the user - secho("User abborted download", fg="red") + cout("User aborted download", style="red") sys.exit(1) except IOError as exc: - secho("I/O error while downloading", fg="red") - secho(str(exc), fg="red") + cout("I/O error while downloading", style="red") + cout(str(exc), style="red") sys.exit(1) except util.ApioException: - secho("Error: package not found", fg="red") + cerror("Package not found") sys.exit(1) # -- Return the destination path @@ -130,7 +130,7 @@ def _unpack_package_file(package_file: Path, package_dir: Path) -> None: # -- Exit if error. if not ok: - secho(f"Error: failed to unpack package file {package_file}", fg="red") + cerror(f"Failed to unpack package file {package_file}") sys.exit(1) @@ -145,8 +145,8 @@ def _parse_package_spec(package_spec: str) -> Tuple[str, str]: """ tokens = package_spec.split("@") if len(tokens) not in [1, 2]: - secho(f"Error: invalid package spec '{package_spec}", fg="red") - secho("Try 'my_package' or 'my_package@0.1.2'", fg="yellow") + cerror(f"Invalid package spec '{package_spec}") + cout("Try 'my_package' or 'my_package@0.1.2'", style="yellow") sys.exit(1) package_name = tokens[0] @@ -166,47 +166,61 @@ def _delete_package_dir( dir_found = package_dir.is_dir() if dir_found: if verbose: - secho(f"Deleting {str(package_dir)}") + cout(f"Deleting {str(package_dir)}") # -- Sanity check the path and delete. assert "packages" in str(package_dir).lower(), package_dir shutil.rmtree(package_dir) if package_dir.exists(): - secho( - f"Error: directory deletion failed: {str(package_dir.absolute())}", - fg="yellow", - ) + cerror(f"Directory deletion failed: {str(package_dir.absolute())}") sys.exit(1) return dir_found +def scan_and_fix_packages( + apio_ctx: ApioContext, cached_config_ok: bool, verbose=False +) -> bool: + """Scan the packages and fix if there are errors. Returns true + if the packages are installed ok.""" + + # -- Scan the packages. + scan = pkg_util.scan_packages( + apio_ctx, cached_config_ok=cached_config_ok, verbose=verbose + ) + + # -- If there are fixable errors, fix them. + if scan.num_errors_to_fix() > 0: + _fix_packages(apio_ctx, scan) + + # -- Return a flag that indicates if all packages are installed ok. We + # -- use a scan from before the fixing but the fixing does not touch + # -- installed ok packages. + return scan.packages_installed_ok() + + def install_missing_packages_on_the_fly(apio_ctx: ApioContext) -> None: """Install on the fly any missing packages. Does not print a thing if all packages are already ok. This function is intended for on demand package fetching by commands such as apio build, and thus is allowed to use fetched remote config instead of fetching a fresh one.""" - # -- Scan the packages for issues. Since it's an 'on the fly' installation, - # -- We want to reduce its footprint and the connectivity requirements nad - # -- let it use a cached remote config, if available. - scan_results = pkg_util.scan_packages( + # -- Scan and fix broken package. + # -- Since this is a on-the-fly operation, we don't require a fresh + # -- remote config file for required packages versions. + installed_ok = scan_and_fix_packages( apio_ctx, cached_config_ok=True, verbose=False ) - # -- If all ok, we are done. - if scan_results.is_all_ok(): + # -- If all the packages are installed, we are done. + if installed_ok: return - # -- Tracks if we made any change. - work_done = False - - # -- Before we check or install, delete all issues, if any. - if scan_results.num_errors_to_fix(): - fix_packages(apio_ctx, scan_results) - work_done = True - + # -- Here when we need to install some packages. Since we just fixed + # -- we can't have broken or packages with version mismatch, just + # -- installed ok, and not installed. + # -- # -- Get lists of installed and required packages. installed_packages = apio_ctx.profile.packages required_packages_names = apio_ctx.platform_packages.keys() @@ -221,19 +235,17 @@ def install_missing_packages_on_the_fly(apio_ctx: ApioContext) -> None: cached_config_ok=False, verbose=False, ) - work_done = True # -- Here all packages should be ok but we check again just in case. - if work_done: - scan_results = pkg_util.scan_packages( - apio_ctx, cached_config_ok=False, verbose=False + scan_results = pkg_util.scan_packages( + apio_ctx, cached_config_ok=False, verbose=False + ) + if not scan_results.is_all_ok(): + cout( + "Warning: packages issues detected. Use " + "'apio packages list' to investigate.", + style="red", ) - if not scan_results.is_all_ok(): - secho( - "Warning: packages issues detected. Use " - "'apio packages list' to investigate.", - fg="red", - ) # pylint: disable=too-many-branches @@ -252,7 +264,7 @@ def install_package( e.b. 'drivers', 'drivers@1.2.0'. 'force' indicates if to perform the installation even if a matching package is already installed. - `verbose` indicates if to print extra information. + 'verbose' indicates if to print extra information. Returns normally if no error, exits the program with an error status and a user message if an error is detected. @@ -263,10 +275,10 @@ def install_package( # -- Exit if no such package for this platform. if package_name not in apio_ctx.platform_packages: - secho(f"Error: no such package '{package_name}'", fg="red") + cerror(f"No such package '{package_name}'") sys.exit(1) - secho(f"Installing package '{package_spec}'", fg="magenta", bold=True) + cout(f"Installing apio package '{package_spec}'", style="magenta") # -- If the user didn't specify a target version we use the one specified # -- in the remote config. @@ -275,9 +287,9 @@ def install_package( package_name, cached_config_ok=cached_config_ok, verbose=verbose ) - secho(f"Target version {target_version}") + cout(f"Target version {target_version}") - # -- If not focring and the target version already installed nothing to do. + # -- If not forcing and the target version already installed nothing to do. if not force_reinstall: # -- Get the version of the installed package, None otherwise. installed_version = apio_ctx.profile.get_package_installed_version( @@ -285,15 +297,14 @@ def install_package( ) if verbose: - print(f"Installed version {installed_version}") + cout(f"Installed version {installed_version}", style="green") # -- If the installed and the target versions are the same then # -- nothing to do. if target_version == installed_version: - secho( + cout( f"Version {target_version} was already installed", - fg="green", - bold=True, + style="green", ) return @@ -302,21 +313,21 @@ def install_package( apio_ctx, package_name, target_version ) if verbose: - print(f"Download URL: {download_url}") + cout(f"Download URL: {download_url}") # -- Prepare the packages directory. apio_ctx.packages_dir.mkdir(exist_ok=True) # -- Prepare the package directory. package_dir = apio_ctx.get_package_dir(package_name) - print(f"Package dir: {package_dir}") + cout(f"Package dir: {package_dir}") - # -- Downlod the package file from the remote server. + # -- Download the package file from the remote server. local_package_file = _download_package_file( download_url, apio_ctx.packages_dir ) if verbose: - print(f"Local package file: {local_package_file}") + cout(f"Local package file: {local_package_file}") # -- Delete the old package dir, if exists, to avoid name conflicts and # -- left over files. @@ -327,7 +338,7 @@ def install_package( # -- Remove the package file. We don't need it anymore. if verbose: - print(f"Deleting package file {local_package_file}") + cout(f"Deleting package file {local_package_file}") local_package_file.unlink() # -- Add package to profile and save. @@ -335,11 +346,7 @@ def install_package( # apio_ctx.profile.save() # -- Inform the user! - secho( - f"Package '{package_name}' installed successfully", - fg="green", - bold=True, - ) + cout(f"Package '{package_name}' installed successfully", style="green") def uninstall_package( @@ -352,10 +359,10 @@ def uninstall_package( package_info = apio_ctx.platform_packages.get(package_name, None) if not package_info: - secho(f"Error: no such package '{package_name}'", fg="red") + cerror(f"No such package '{package_name}'") sys.exit(1) - secho(f"Uninstalling package '{package_name}'") + cout(f"Uninstalling apio package '{package_name}'", style="magenta") # -- Remove the folder with all its content!! dir_existed = _delete_package_dir(apio_ctx, package_name, verbose) @@ -372,39 +379,38 @@ def uninstall_package( if dir_existed or installed_version: # -- Inform the user - secho( - f"Package '{package_name}' uninstalled successfully", - fg="green", - bold=True, + cout( + f"Package '{package_name}' uninstalled successfully", style="green" ) else: # -- Package not installed. We treat it as a success. - secho(f"Package '{package_name}' was not installed", fg="green") + cout( + f"Package '{package_name}' was already uninstalled", style="green" + ) -def fix_packages( +def _fix_packages( apio_ctx: ApioContext, scan: pkg_util.PackageScanResults ) -> None: """If the package scan result contains errors, fix them.""" - # -- Fix broken packages. - for package_name in scan.bad_version_package_names_subset: - print(f"Uninstalling versin mismatch '{package_name}'") + for package_name in scan.bad_version_package_names: + cout(f"Uninstalling bad version of '{package_name}'", style="magenta") _delete_package_dir(apio_ctx, package_name, verbose=False) apio_ctx.profile.remove_package(package_name) for package_name in scan.broken_package_names: - print(f"Uninstalling broken package '{package_name}'") + cout(f"Uninstalling broken package '{package_name}'", style="magenta") _delete_package_dir(apio_ctx, package_name, verbose=False) apio_ctx.profile.remove_package(package_name) for package_name in scan.orphan_package_names: - print(f"Uninstalling unknown package '{package_name}'") + cout(f"Uninstalling unknown package '{package_name}'", style="magenta") apio_ctx.profile.remove_package(package_name) for dir_name in scan.orphan_dir_names: - print(f"Deleting unknown package dir '{dir_name}'") - # -- Sanity check. Since apio_ctx.packages_dir is guarranted to include + cout(f"Deleting unknown package dir '{dir_name}'", style="magenta") + # -- Sanity check. Since apio_ctx.packages_dir is guaranteed to include # -- the word packages, this can fail only due to programming error. dir_path = apio_ctx.packages_dir / dir_name assert "packages" in str(dir_path).lower(), dir_path @@ -412,8 +418,8 @@ def fix_packages( shutil.rmtree(dir_path) for file_name in scan.orphan_file_names: - print(f"Deleting unknown package file '{file_name}'") - # -- Sanity check. Since apio_ctx.packages_dir is guarranted to + cout(f"Deleting unknown package file '{file_name}'", style="magenta") + # -- Sanity check. Since apio_ctx.packages_dir is guaranteed to # -- include the word packages, this can fail only due to programming # -- error. file_path = apio_ctx.packages_dir / file_name diff --git a/apio/managers/programmers.py b/apio/managers/programmers.py index 5a10aae7..6c092306 100644 --- a/apio/managers/programmers.py +++ b/apio/managers/programmers.py @@ -7,11 +7,11 @@ # -- This file is part of the Apio project # -- (C) 2016-2019 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 import re import sys -from click import secho +from apio.common.apio_console import cout, cerror from apio.utils import util, pkg_util from apio.managers.system import System from apio.apio_context import ApioContext @@ -57,12 +57,12 @@ def construct_programmer_cmd( # -- # -- Special case for the TinyFPGA on MACOS platforms # -- TinyFPGA BX board is not detected in MacOS HighSierra - if "tinyprog" in board_info and apio_ctx.is_darwin(): + if "tinyprog" in board_info and apio_ctx.is_darwin: # In this case the serial check is ignored # This is the command line to execute for uploading the # circuit if util.is_debug(): - print("Applying a special case for tinyprog on darwin.") + cout("Applying a special case for tinyprog on darwin.") return "tinyprog --libusb --program $SOURCE" # -- Serialize programmer command @@ -77,7 +77,7 @@ def construct_programmer_cmd( apio_ctx, board_info, sram, flash ) if util.is_debug(): - print(f"Programmer template: [{programmer}]") + cout(f"Programmer template: [{programmer}]") # -- The placeholder for the bitstream file name should always exist. assert "$SOURCE" in programmer, programmer @@ -109,7 +109,7 @@ def construct_programmer_cmd( # -- We force an early env setting message to have # -- the programmer message closer to the error message. pkg_util.set_env_for_packages(apio_ctx) - secho("Querying programmer parameters.") + cout("Querying programmer parameters.") # -- Check that the board is connected # -- If not, an exception is raised @@ -129,7 +129,7 @@ def construct_programmer_cmd( # -- We force an early env setting message to have # -- the programmer message closer to the error message. pkg_util.set_env_for_packages(apio_ctx) - secho("Querying serial port parameters.") + cout("Querying serial port parameters.") # -- Check that the board is connected _check_usb(apio_ctx, board, board_info) @@ -230,7 +230,7 @@ def _check_usb(apio_ctx: ApioContext, board: str, board_info: dict) -> None: # -- If it does not have the "usb" property, it means # -- the board configuration is wrong...Raise an exception if "usb" not in board_info: - secho("Missing board configuration: usb", fg="red") + cerror("Missing board configuration: usb") sys.exit(1) # -- Get the vid and pid from the configuration @@ -246,7 +246,7 @@ def _check_usb(apio_ctx: ApioContext, board: str, board_info: dict) -> None: connected_devices = system.get_usb_devices() if util.is_debug(): - print(f"usb devices: {connected_devices}") + cout(f"usb devices: {connected_devices}") # -- Check if the given device (vid:pid) is connected! # -- Not connected by default @@ -261,17 +261,17 @@ def _check_usb(apio_ctx: ApioContext, board: str, board_info: dict) -> None: # -- The board is NOT connected if not found: + cerror("Board " + board + " not connected") # -- Special case! TinyFPGA board # -- Maybe the board is NOT detected because # -- the user has not press the reset button and the bootloader # -- is not active if "tinyprog" in board_info: - secho( + cout( "Activate bootloader by pressing the reset button", - fg="yellow", + style="yellow", ) - secho("board " + board + " not connected", fg="red") sys.exit(1) @@ -294,7 +294,7 @@ def _get_serial_port( # -- Board not connected if not device: - secho("board " + board + " not connected", fg="red") + cerror("Board " + board + " not connected") sys.exit(1) # -- Board connected. Return the serial port detected @@ -317,7 +317,7 @@ def _check_serial(board: str, board_info: dict, ext_serial_port: str) -> str: # -- If it does not have the "usb" property, it means # -- the board configuration is wrong...Raise an exception if "usb" not in board_info: - secho("Missing board configuration: usb", fg="red") + cerror("Missing board configuration: usb") sys.exit(1) # -- Get the vid and pid from the configuration @@ -334,11 +334,11 @@ def _check_serial(board: str, board_info: dict, ext_serial_port: str) -> str: serial_ports = util.get_serial_ports() if util.is_debug(): - print(f"serial ports: {serial_ports}") + cout(f"serial ports: {serial_ports}") # -- If no serial ports detected, exit with an error. if not serial_ports: - secho("board " + board + " not available", fg="red") + cerror("Board " + board + " not available") sys.exit(1) # -- Match the discovered serial ports @@ -440,7 +440,7 @@ def _get_ftdi_id(apio_ctx: ApioContext, board, board_info, ext_ftdi_id) -> str: # -- No FTDI board connected if ftdi_id is None: - secho("board " + board + " not connected", fg="red") + cerror("Board " + board + " not connected") sys.exit(1) # -- Return the FTDI index @@ -467,7 +467,7 @@ def _check_ftdi( # -- Check that the given board has the property "ftdi" # -- If not, it is an error. Raise an exception if "ftdi" not in board_info: - secho("Missing board configuration: ftdi", fg="red") + cerror("Missing board configuration: ftdi") sys.exit(1) # -- Get the board description from the the apio database @@ -490,7 +490,7 @@ def _check_ftdi( # -- No FTDI devices detected --> Error! if not connected_devices: - secho("board " + board + " not available", fg="red") + cerror("Board " + board + " not available") sys.exit(1) # -- Check if the given board is connected diff --git a/apio/managers/project.py b/apio/managers/project.py index b03503e4..bac6d836 100644 --- a/apio/managers/project.py +++ b/apio/managers/project.py @@ -2,11 +2,11 @@ # -- This file is part of the Apio project # -- (C) 2016-2018 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """Utility functionality for apio click commands. """ import sys @@ -16,10 +16,8 @@ from pathlib import Path from typing import Dict, Optional, Union, Any, List from configobj import ConfigObj -from click import secho +from apio.common.apio_console import cout, cerror -# -- Apio projecto filename -APIO_INI = "apio.ini" DEFAULT_TOP_MODULE = "main" @@ -28,26 +26,91 @@ https://github.com/FPGAwars/apio/wiki/Project-configuration-file """ -# -- Set of options every valid project should have. -REQUIRED_OPTIONS = { - # -- The board name. - "board", -} +# -- The options docs here are formatted in the markdown format of the +# -- python rich library. See apio_docs_apio_ini.py to see how they are +# -- used. +BOARD_OPTION_DOC = """ +The option 'board' specifies the board definition that is used by the \ +project. The board ID must be one of the board IDs, such as 'alhambra-ii', \ +that is listed by the command 'apio boards'. + +Example:[code] + board = alhambra-ii[/code] + +Apio uses the board ID to determine information such as the FPGA part number \ +and the programmer command to use to upload the design to the board. + +Apio has resource files with definitions of boards, FPGAs, and programmers. \ +If the project requires custom definitions, you can add custom \ +'boards.jsonc', 'fpgas.jsonc', and 'programmers.jsonc' files in the project \ +directory, and Apio will use them instead. +""" + +TOP_MODULE_OPTION_DOC = """ +The option 'top-module' specifies the name of the top module of the design. \ +If 'top-module' is not specified, Apio assumes the default name 'main'; \ +however, it is a good practice to always explicitly specify the top module. + +Example:[code] + top-module = my_main[/code] +""" + +DEFAULT_TESTBENCH_DOC = """ +The option 'default-testbench' is useful in projects that have more than \ +a single testbench file, because it allows specifying the default testbench \ +that will be simulated when the command 'apio sim' is run without a testbench \ +argument. + +Without this option, Apio will exit with an error message if the project \ +contains more than one testbench file and a testbench was not specified in \ +the 'apio sim' command. + +Example:[code] + default-testbench = my_module_tb.v[/code] +""" + +FORMAT_VERIBLE_OPTIONS_DOC = """ +The option 'format-verible-options' allows controlling the operation of the \ +'apio format' command by specifying additional options to the underlying \ +'verible' formatter. + +Example:[code] + format-verible-options = + --column_limit=80 + --indentation_spaces=4[/code] + +For the list of the Verible formatter options, run the command \ +'apio raw -- verible-verilog-format --helpfull' +""" + +YOSYS_SYNTH_EXTRA_OPTIONS_DOC = """ +The option 'yosys-synth-extra-options' allows adding options to the \ +yosys synth command. In the example below, it adds the option '-dsp', \ +which enables for some FPGAs the use of DSP cells to implement multiply \ +operations. This is an advanced and esoteric option that is typically \ +not needed. + +Example:[code] + yosys-synth-extra-options = -dsp[/code] +""" -# -- Set of additional options a project may have. -OPTIONAL_OPTIONS = { +OPTIONS = { + # -- The board name. + "board": BOARD_OPTION_DOC, # -- The top module name. Default is 'main'. - "top-module", + "top-module": TOP_MODULE_OPTION_DOC, # -- The default testbench name for 'apio sim'. - "default-testbench", + "default-testbench": DEFAULT_TESTBENCH_DOC, # -- Multi line list of verible options for 'apio format' - "format-verible-options", + "format-verible-options": FORMAT_VERIBLE_OPTIONS_DOC, # -- Additional option for the yosys synth command (inside the -p arg). - "yosys-synth-extra-options", + "yosys-synth-extra-options": YOSYS_SYNTH_EXTRA_OPTIONS_DOC, } -# -- Set of all options a project may have. -ALL_OPTIONS = REQUIRED_OPTIONS | OPTIONAL_OPTIONS +# -- The subset of the options in OPTIONS that are required. +REQUIRED_OPTIONS = { + "board", +} # pylint: disable=too-few-public-methods @@ -90,17 +153,13 @@ def _validate(self, resolver: ProjectResolver): # -- Check that all the required options are present. for option in REQUIRED_OPTIONS: if option not in self._options: - secho( - f"Error: missing option '{option}' in {APIO_INI}.", - fg="red", - ) + cerror(f"Missing option '{option}' in apio.ini.") sys.exit(1) # -- Check that there are no unknown options. - supported_options = ALL_OPTIONS for option in self._options: - if option not in supported_options: - secho(f"Error: unknown project option '{option}'", fg="red") + if option not in OPTIONS: + cerror(f"Unknown project option '{option}'") sys.exit(1) # -- Force 'board' to have the canonical name of the board. @@ -112,16 +171,16 @@ def _validate(self, resolver: ProjectResolver): # -- 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 - secho( + cout( "Project file has no 'top-module', " f"assuming '{DEFAULT_TOP_MODULE}'.", - fg="yellow", + style="yellow", ) def get(self, option: str, default: Any = None) -> Union[str, Any]: """Lookup an option value by name. Returns default if not found.""" # -- If this fails, this is a programming error. - assert option in ALL_OPTIONS, f"Invalid project option: [{option}]" + assert option in OPTIONS, f"Invalid project option: [{option}]" # -- Lookup with default return self._options.get(option, default) @@ -136,7 +195,7 @@ def get_as_lines_list( ) -> Union[List[str], Any]: """Lookup an option value that has a line list format. Returns the list of non empty lines or default if no value. Option - must be in ALL_OPTIONS.""" + must be in OPTIONS.""" # -- Get the raw value. values = self.get(option, None) @@ -165,11 +224,11 @@ def load_project_from_file( call its validate() method.""" # -- Construct the apio.ini path. - file_path = project_dir / APIO_INI + file_path = project_dir / "apio.ini" # -- Currently, apio.ini is still optional so we just warn. if not file_path.exists(): - secho(f"Error: missing project file {APIO_INI}.", fg="red") + cerror("Missing project file apio.ini.") sys.exit(1) # -- Read and parse the file. @@ -178,15 +237,12 @@ def load_project_from_file( # -- Should contain an [env] section. if "env" not in parser.sections(): - secho(f"Error: {APIO_INI} has no [env] section.", fg="red") + cerror("The file apio.ini has no [env] section.") sys.exit(1) # -- Should not contain any other section. if len(parser.sections()) > 1: - secho( - f"Error: {APIO_INI} should contain only an [env] section.", - fg="red", - ) + cerror("The file apio.ini should contain only an [env] section.") sys.exit(1) # -- Collect the name/value pairs. @@ -206,16 +262,15 @@ def create_project_file( """Creates a new apio project file. Exits on any error.""" # -- Construct the path - ini_path = project_dir / APIO_INI + ini_path = project_dir / "apio.ini" # -- Error if apio.ini already exists. if ini_path.exists(): - - secho(f"Error: the file {APIO_INI} already exists.", fg="red") + cerror("The file apio.ini already exists.") sys.exit(1) # -- Construct and write the apio.ini file.. - secho(f"Creating {ini_path} file ...") + cout(f"Creating {ini_path} file ...") config = ConfigObj(str(ini_path)) config.initial_comment = TOP_COMMENT.split("\n") @@ -224,8 +279,4 @@ def create_project_file( config["env"]["top-module"] = top_module config.write() - secho( - f"The file '{ini_path}' was created successfully.", - fg="green", - bold=True, - ) + cout(f"The file '{ini_path}' was created successfully.", style="green") diff --git a/apio/managers/scons.py b/apio/managers/scons.py index 3b36f69a..abb4c1ca 100644 --- a/apio/managers/scons.py +++ b/apio/managers/scons.py @@ -7,27 +7,27 @@ # -- This file is part of the Apio project # -- (C) 2016-2019 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 import traceback import os import sys import time -from pathlib import Path import shutil from functools import wraps from datetime import datetime -import click -from click import secho from google.protobuf import text_format +from apio.common.apio_console import cout, cerror, cstyle from apio.utils import util, pkg_util +from apio.common.apio_consts import BUILD_DIR from apio.apio_context import ApioContext from apio.managers.scons_filter import SconsFilter from apio.managers import installer from apio.profile import Profile -from apio.proto.apio_pb2 import ( +from apio.common import rich_lib_windows +from apio.common.proto.apio_pb2 import ( Verbosity, - Envrionment, + Environment, SconsParams, TargetParams, FpgaInfo, @@ -58,7 +58,7 @@ # -- Based on # -- https://stackoverflow.com/questions/5929107/decorators-with-parameters def on_exception(*, exit_code: int): - """Decoractor for functions that return int exit code. If the function + """Decorator for functions that return int exit code. If the function throws an exception, the error message is printed, and the caller see the returned value exit_code instead of the exception. """ @@ -73,7 +73,7 @@ def wrapper(*args, **kwargs): traceback.print_tb(exc.__traceback__) if str(exc): - secho("Error: " + str(exc), fg="red") + cerror(str(exc)) return exit_code return wrapper @@ -99,7 +99,7 @@ def clean(self) -> int: scons_params = self.construct_scons_params() - # --Clean the project: run scons -c (with aditional arguments) + # --Clean the project: run scons -c (with additional arguments) return self._run("-c", scons_params=scons_params, uses_packages=False) @on_exception(exit_code=1) @@ -287,10 +287,7 @@ def construct_scons_params( GowinFpgaInfo(family=fpga_config["type"]) ) else: - secho( - f"Internal error: unexpected fpga_arch value {fpga_arch}", - fg="red", - ) + cerror(f"Unexpected fpga_arch value {fpga_arch}") sys.exit(1) # -- We are done populating The FpgaInfo params.. @@ -305,15 +302,16 @@ def construct_scons_params( assert apio_ctx.platform_id, "Missing platform_id in apio context" oss_vars = apio_ctx.all_packages["oss-cad-suite"]["env"]["vars"] - result.envrionment.MergeFrom( - Envrionment( + result.environment.MergeFrom( + Environment( platform_id=apio_ctx.platform_id, + is_windows=apio_ctx.is_windows, is_debug=util.is_debug(), yosys_path=oss_vars["YOSYS_LIB"], trellis_path=oss_vars["TRELLIS"], ) ) - assert result.envrionment.IsInitialized(), result + assert result.environment.IsInitialized(), result # -- Populate the Project params. result.project.MergeFrom( @@ -327,11 +325,17 @@ def construct_scons_params( ) assert result.project.IsInitialized(), result - # -- Populate the optinal command specific params. + # -- Populate the optional command specific params. if target_params: result.target.MergeFrom(target_params) assert result.target.IsInitialized(), result + # -- If windows, populate the rich library workaround parameters. + if apio_ctx.is_windows: + result.rich_lib_windows_params.MergeFrom( + rich_lib_windows.get_workaround_params() + ) + # -- All done. assert result.IsInitialized(), result return result @@ -341,7 +345,7 @@ def construct_scons_params( # pylint: disable=too-many-positional-arguments def _run( self, - scond_command: str, + scons_command: str, *, scons_params: SconsParams = None, uses_packages: bool, @@ -354,7 +358,7 @@ def _run( scons_file_path = scons_dir / "SConstruct" variables = ["-f", f"{scons_file_path}"] - # -- Pass to the wscons process the timestamp of the scons params we + # -- Pass to the scons process the timestamp of the scons params we # -- pass via a file. This is for verification purposes only. variables += [f"timestamp={scons_params.timestamp}"] @@ -369,12 +373,12 @@ def _run( pkg_util.set_env_for_packages(self.apio_ctx) if util.is_debug(): - secho("\nSCONS CALL:", fg="magenta") - secho(f"* command: {scond_command}") - secho(f"* variables: {variables}") - secho(f"* uses packages: {uses_packages}") - secho(f"* scons params: \n{scons_params}") - secho() + cout("\nSCONS CALL:", style="magenta") + cout(f"* command: {scons_command}") + cout(f"* variables: {variables}") + cout(f"* uses packages: {uses_packages}") + cout(f"* scons params: \n{scons_params}") + cout() # -- Get the terminal width (typically 80) terminal_width, _ = shutil.get_terminal_size() @@ -383,19 +387,14 @@ def _run( # -- to execute the apio command) start_time = time.time() - # -- Get the date as a string - date_time_str = datetime.now().strftime("%c") - # -- Board name string in color - board_color = click.style( - scons_params.project.board_id, fg="cyan", bold=True - ) + styled_board_id = cstyle(scons_params.project.board_id, style="cyan") # -- Print information on the console - secho(f"[{date_time_str}] Processing {board_color}") + cout(f"Processing board {styled_board_id}") # -- Print a horizontal line - secho("-" * terminal_width, bold=True) + cout("-" * terminal_width) # -- Create the scons debug options. See details at # -- https://scons.org/doc/2.4.1/HTML/scons-man.html @@ -407,21 +406,20 @@ def _run( # -- Command to execute: scons -Q apio_cmd flags scons_command = ( - ["scons"] + ["-Q", scond_command] + debug_options + variables + ["scons"] + ["-Q", scons_command] + debug_options + variables ) - # -- An output filter that manupulates the scons stdout/err lines as + # -- An output filter that manipulates the scons stdout/err lines as # -- needed and write them to stdout. - colors_enabled = Profile.read_color_prefernces() + colors_enabled = Profile.read_color_preferences() scons_filter = SconsFilter(colors_enabled) # -- Write the scons parameters to a temp file in the build # -- directory. It will be cleaned up as part of 'apio cleanup'. # -- At this point, the project is the current directory, even if # -- the command used the --project-dir option. - build_dir = Path("_build") - os.makedirs(build_dir, exist_ok=True) - with open(build_dir / "scons.params", "w", encoding="utf8") as f: + os.makedirs(BUILD_DIR, exist_ok=True) + with open(BUILD_DIR / "scons.params", "w", encoding="utf8") as f: f.write(text_format.MessageToString(scons_params)) # -- Execute the scons builder! @@ -445,13 +443,13 @@ def _run( # -- Status message status = ( - click.style(" ERROR ", fg="red", bold=True) + cstyle(" ERROR ", style="red") if is_error - else click.style("SUCCESS", fg="green", bold=True) + else cstyle("SUCCESS", style="green") ) # -- Print the summary line. - secho(f"{half_line} [{status}]{summary_text}{half_line}", err=is_error) + cout(f"{half_line} [{status}]{summary_text}{half_line}") # -- Return the exit code return result.exit_code diff --git a/apio/managers/scons_filter.py b/apio/managers/scons_filter.py index 5e2c3d6a..486d1de0 100644 --- a/apio/managers/scons_filter.py +++ b/apio/managers/scons_filter.py @@ -4,27 +4,33 @@ # -- This file is part of the Apio project # -- (C) 2016-2019 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # pylint: disable=fixme # TODO: Implement range detectors for Fumo, Tinyprog, and Iceprog, similar to # the pnr detector. This will avoid matching of output from other programs. -# TODO: Use util.get_terminal_config() to determine if the output goes to a +# TODO: Use apio_console.is_terminal() to determine if the output goes to a # terminal or a pipe and have an alternative handling for the cursor commands # when writing to a pipe. import re from enum import Enum from typing import List, Optional, Tuple -from click import secho, echo -from click.termui import unstyle +from apio.common.apio_console import cout # -- Terminal cursor commands. CURSOR_UP = "\033[F" ERASE_LINE = "\033[K" +# -- A regex to detect iverilog warnings we want to filter out, per +# -- https://github.com/FPGAwars/apio/issues/557 +IVERILOG_TIMING_WARNING_REGEX = re.compile( + r"oss-cad-suite/share/yosys.*warning.*Timing checks are not supported", + re.IGNORECASE, +) + class PipeId(Enum): """Represent the two output streams from the scons subprocess.""" @@ -168,31 +174,6 @@ def on_stderr_line(self, line: str) -> None: """Stderr pipe calls this on each line.""" self.on_line(PipeId.STDERR, line) - def emit_line( - self, line: str, *, fg: str = None, bold: bool = None - ) -> None: - """Emit a line from the scons filter. We use scecho only when thre - is an explicit color, to avoid interfering with color from the scons - job output, for example for color that spans lines as in - 'secho("line1\nline2", fg="red", color=True)'. - """ - # -- If we run with colors turned of, remove any ansi colors that can - # -- come from the scons process. - if not self.colors_enabled: - fg = None - bold = None - line = unstyle(line) - - # -- Echo the line. - if fg or bold: - secho(line, fg=fg, bold=bold) - else: - # -- In this case we use echo to preserve ansi colors from the - # -- scons job which can span multiple ines. Using secho would - # -- interfere with those colors, preserving only the color of the - # -- first line. - echo(line) - @staticmethod def _assign_line_color( line: str, patterns: List[Tuple[str, str]], default_color: str = None @@ -207,6 +188,7 @@ def _assign_line_color( return default_color # pylint: disable=too-many-return-statements + # pylint: disable=too-many-branches def on_line(self, pipe_id: PipeId, line: str) -> None: """A shared handler for stdout/err lines from the scons sub process. The handler writes both stdout and stderr lines to stdout, possibly @@ -224,6 +206,14 @@ def on_line(self, pipe_id: PipeId, line: str) -> None: in_iverolog_range = self._iverilog_detector.update(pipe_id, line) in_iceprog_range = self._iceprog_detector.update(pipe_id, line) + # -- For debugging. + # cout( + # f"{'P' if in_pnr_verbose_range else '-'}" + # f"{'V' if in_iverolog_range else '-'}" + # f"{'I' if in_iverolog_range else '-'}" + # f" {pipe_id} : {line}" + # ) + # -- Handle the line while in the nextpnr verbose log range. if pipe_id == PipeId.STDERR and in_pnr_verbose_range: @@ -238,9 +228,10 @@ def on_line(self, pipe_id: PipeId, line: str) -> None: [ (r"^warning:", "yellow"), (r"^error:", "red"), + (r"^fatal error:", "red"), ], ) - self.emit_line(line, fg=line_color) + cout(line, style=line_color) return # -- Special handling of iverilog lines. We drop warning line spam @@ -297,7 +288,7 @@ def on_line(self, pipe_id: PipeId, line: str) -> None: (r"^VERIFY OK", "green"), ], ) - self.emit_line(line, fg=line_color) + cout(line, style=line_color) return # -- Special handling for Fumo lines. @@ -314,7 +305,7 @@ def on_line(self, pipe_id: PipeId, line: str) -> None: # - Commit 93fc9bc4f3bfd21568e2d66f11976831467e3b97. # print(CURSOR_UP + ERASE_LINE, end="", flush=True) - self.emit_line(line, fg="green", bold=True) + cout(line, style="green") return # -- Special handling for tinyprog lines. @@ -342,12 +333,18 @@ def on_line(self, pipe_id: PipeId, line: str) -> None: # - Commit 93fc9bc4f3bfd21568e2d66f11976831467e3b97. # print(CURSOR_UP + ERASE_LINE, end="", flush=True) - self.emit_line(line) + cout(line) return - # Handling the rest of the stdout lines. + # -- Special filter for https://github.com/FPGAwars/apio/issues/557 + if IVERILOG_TIMING_WARNING_REGEX.search(line): + # -- Ignore this line. + # cout(line, style="magenta") + return + + # -- Handling the rest of the stdout lines. if pipe_id == PipeId.STDOUT: - # Default stdout line coloring. + # -- Default stdout line coloring. line_color = self._assign_line_color( line.lower(), [ @@ -356,7 +353,7 @@ def on_line(self, pipe_id: PipeId, line: str) -> None: (r"^error:", "red"), ], ) - self.emit_line(line, fg=line_color) + cout(line, style=line_color) return # Handling the rest of stderr the lines. @@ -368,4 +365,4 @@ def on_line(self, pipe_id: PipeId, line: str) -> None: (r"^error:", "red"), ], ) - self.emit_line(line, fg=line_color) + cout(line, style=line_color) diff --git a/apio/managers/system.py b/apio/managers/system.py index 1dadfa13..a213c4bd 100644 --- a/apio/managers/system.py +++ b/apio/managers/system.py @@ -4,12 +4,11 @@ # -- This file is part of the Apio project # -- (C) 2016-2019 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 import re import sys -from click import secho - +from apio.common.apio_console import cout, cerror from apio.utils import util, pkg_util from apio.apio_context import ApioContext from apio.managers import installer @@ -26,17 +25,15 @@ def _lsftdi_fatal_error(self, result: util.CommandResult) -> None: """Handles a failure of a 'lsftdi' command. Print message and exits.""" # assert result.exit_code != 0, result - secho(result.out_text) - secho(f"{result.err_text}", fg="red") - secho("Error: the 'lsftdi' command failed.", fg="red") + cout(result.out_text) + cerror("The 'lsftdi' command failed.", result.err_text) # -- A special hint for zadig on windows. - if self.apio_ctx.is_windows(): - secho( - "\n" + if self.apio_ctx.is_windows: + cout( "[Hint]: did you install the ftdi driver using " "'apio drivers --ftdi-install'?", - fg="yellow", + style="yellow", ) sys.exit(1) @@ -56,22 +53,22 @@ def lsftdi(self) -> int: # -- Print error message and exit. self._lsftdi_fatal_error(result) - secho(result.out_text) + cout(result.out_text) return 0 def lsserial(self) -> int: """List the serial ports. Returns exit code.""" serial_ports = util.get_serial_ports() - secho(f"Number of Serial devices found: {len(serial_ports)}") + cout(f"Number of Serial devices found: {len(serial_ports)}") for serial_port in serial_ports: port = serial_port.get("port") description = serial_port.get("description") hwid = serial_port.get("hwid") - secho(port, fg="cyan", bold=True) - secho(f"Description: {description}") - secho(f"Hardware info: {hwid}\n") + cout(port, style="cyan") + cout(f"Description: {description}") + cout(f"Hardware info: {hwid}\n") return 0 @@ -93,8 +90,8 @@ def get_usb_devices(self) -> list: result = self._run_command("lsusb", silent=True) if result.exit_code != 0: - secho(result.out_text) - secho(result.err_text, fg="red") + cout(result.out_text) + cout(result.err_text, style="red") raise RuntimeError("Error executing lsusb") # -- Get the list of the usb devices. It is read @@ -161,7 +158,7 @@ def _run_command( # pylint: disable=fixme # TODO: Is this necessary or does windows accepts commands without # the '.exe' extension? - if self.apio_ctx.is_windows(): + if self.apio_ctx.is_windows: command = command + ".exe" # -- Set the stdout and stderr callbacks, when executing the command @@ -184,14 +181,14 @@ def _on_stdout(line): """Callback function. It is executed when the command prints information on the standard output """ - secho(line) + cout(line) @staticmethod def _on_stderr(line): """Callback function. It is executed when the command prints information on the standard error """ - secho(line, fg="red") + cout(line, style="red") @staticmethod def _parse_usb_devices(text: str) -> list: diff --git a/apio/managers/unpacker.py b/apio/managers/unpacker.py index 35c49f99..b078618c 100644 --- a/apio/managers/unpacker.py +++ b/apio/managers/unpacker.py @@ -4,18 +4,18 @@ # -- This file is part of the Apio project # -- (C) 2016-2019 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 from os import chmod from pathlib import Path from tarfile import open as tarfile_open from zipfile import ZipFile -import click -from click import secho +from rich.progress import track +from apio.common.apio_console import cout, console from apio.utils import util @@ -123,7 +123,7 @@ def __init__(self, archpath: Path, dest_dir=Path(".")): # -- Archive type not known!! Raise an exception! if not self._unpacker: - secho(f"Can not unpack file '{archpath}'") + cout(f"Can not unpack file '{archpath}'") raise util.ApioException() def start(self) -> bool: @@ -132,19 +132,12 @@ def start(self) -> bool: # -- Build an array with all the files inside the tarball items = self._unpacker.get_items() - # -- Progress bar... - with click.progressbar( - items, - length=len(items), - label=click.style("Unpacking..", fg="yellow"), - fill_char=click.style("█", fg="cyan", bold=True), - empty_char=click.style("░", fg="cyan", bold=True), - ) as pbar: - - # -- Go though all the files in the archive... - for item in pbar: - - # -- Extract the file! - self._unpacker.extract_item(item, self._dest_dir) + # -- Unpack while displaying a progress bar. + for i in track( + range(len(items)), + description="Unpacking ", + console=console(), + ): + self._unpacker.extract_item(items[i], self._dest_dir) return True diff --git a/apio/profile.py b/apio/profile.py index 68f83c15..d00f32d3 100644 --- a/apio/profile.py +++ b/apio/profile.py @@ -2,21 +2,18 @@ # -- This file is part of the Apio project # -- (C) 2016-2019 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 """Manage the apio profile file""" import json import sys from typing import Union, Any, Dict from pathlib import Path -import click -from click import secho import requests +from apio.common import apio_console +from apio.common.apio_console import cout, cerror, cprint from apio.utils import util -# -- Template for remote config file url. The placeholder is for the -# -- apio verison such as "0.9.6". - class Profile: """Class for managing the apio profile file @@ -37,7 +34,7 @@ def __init__(self, home_dir: Path, remote_config_url_template: str): assert "%" not in self.remote_config_url, self.remote_config_url if util.is_debug(): - print(f"Remote config url: {self.remote_config_url}") + cout(f"Remote config url: {self.remote_config_url}") # ---- Set the default parameters @@ -78,7 +75,7 @@ def add_setting(self, key: str, value: str): self._save() def set_preferences_colors(self, value: str): - """Set the colors prefernes to on or off.""" + """Set the colors preferences to on or off.""" assert value in ["on", "off"], f"Invalid value [{value}]" self.preferences["colors"] = value self._save() @@ -96,16 +93,14 @@ def apply_color_preferences(): """If an colors are disabled and a click context exist, set it up to disable colors. """ + # -- Determine if colors should be on or off. + colors: bool = Profile.read_color_preferences() - colors: bool = Profile.read_color_prefernces() - click_context = click.get_current_context(silent=True) - # If colors are on, we don't write True but None, to use the default - # policy of emitting colors only if not piped out. - if click_context: - click_context.color = None if colors else False + # -- Apply to apio console which controls all output and coloring. + apio_console.configure(colors=colors) @staticmethod - def read_color_prefernces(*, default=True) -> Union[bool, Any]: + def read_color_preferences(*, default=True) -> Union[bool, Any]: """Returns the value of the colors preferences or default if not specified. This is a static method because we may need this value before creating the profile object, for example when printing command @@ -118,7 +113,7 @@ def read_color_prefernces(*, default=True) -> Union[bool, Any]: return default with open(profile_path, "r", encoding="utf8") as f: - # -- Get the colors preferenes value, if exists. + # -- Get the colors preferences value, if exists. data = json.load(f) preferences = data.get("preferences", {}) colors = preferences.get("colors", None) @@ -214,15 +209,15 @@ def _save(self): # -- Dump for debugging. if util.is_debug(): - secho("Saved profile:", fg="magenta") - secho(json.dumps(data)) + cout("Saved profile:", style="magenta") + cprint(json.dumps(data, indent=2)) def _get_remote_config( self, *, cached_config_ok: bool, verbose: bool ) -> Dict: """Returns the apio remote config JSON dict. If the value is cached in the profile and force_cache = False, then it's returned as is, - otherwise, it's fetched remotly and also saved in the profile. + otherwise, it's fetched remotely and also saved in the profile. """ # -- If a remote config is already available and fetch is not force # -- use it. @@ -236,10 +231,7 @@ def _get_remote_config( # apio_version = util.get_apio_version() # config_url = APIO_REMOTE_CONFIG_URL_TEMPLATE.format(apio_version) if verbose or util.is_debug(): - secho( - f"Fetching remote config from '{self.remote_config_url}'", - fg="magenta", - ) + cout(f"Fetching remote config from '{self.remote_config_url}'") # -- Fetch the version info. resp: requests.Response = requests.get( @@ -248,20 +240,22 @@ def _get_remote_config( # -- Exit if http error. if resp.status_code != 200: - secho("Error downloading the remote config file", fg="red") - secho(f"URL {self.remote_config_url}", fg="red") - secho(f"Error code {resp.status_code}", fg="red") + cerror( + "Downloading apio remote config file failed, " + f"error code {resp.status_code}", + ) + cout(f"URL {self.remote_config_url}", style="yellow") sys.exit(1) # -- Here when download was ok. if verbose or util.is_debug(): - secho("Remote config file downloaded ok:\n") + cout("Remote config file downloaded ok.") + # -- Print the file's content. if util.is_debug(): - secho(resp.text, fg="cyan", bold=True) - secho("\n") + cout(resp.text) - # -- Parse the remote JSON config file into adict. + # -- Parse the remote JSON config file into a dict. try: remote_config = json.loads(resp.text) @@ -269,8 +263,7 @@ def _get_remote_config( except json.decoder.JSONDecodeError as exc: # -- Show the error and abort. - secho("Apio System Error! Invalid remote cofing file", fg="red") - secho(f"{exc}\n", fg="red") + cerror("Invalid remote config file", f"{exc}") sys.exit(1) # -- Update the profile and save @@ -278,7 +271,7 @@ def _get_remote_config( self.remote_config = remote_config self._save() - # -- Mark that we have the altest config. + # -- Mark that we have the last config. self.remote_config_fetched = True # -- Return the object for the resource diff --git a/apio/proto/apio_pb2.py b/apio/proto/apio_pb2.py deleted file mode 100644 index c225d520..00000000 --- a/apio/proto/apio_pb2.py +++ /dev/null @@ -1,72 +0,0 @@ - -# pylint: disable=C0114, C0115, C0301, C0303, C0411 -# pylint: disable=E0245, E0602, E1139 -# pylint: disable=R0913, R0801, R0917 -# pylint: disable=W0212, W0223, W0311, W0613, W0622 - -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: apio.proto -# Protobuf Python Version: 5.29.0 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 29, - 0, - '', - 'apio.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\napio.proto\x12\napio.proto\"+\n\rIce40FpgaInfo\x12\x0c\n\x04type\x18\x01 \x02(\t\x12\x0c\n\x04pack\x18\x02 \x02(\t\"9\n\x0c\x45\x63p5FpgaInfo\x12\x0c\n\x04type\x18\x04 \x02(\t\x12\x0c\n\x04pack\x18\x05 \x02(\t\x12\r\n\x05speed\x18\x06 \x02(\t\"\x1f\n\rGowinFpgaInfo\x12\x0e\n\x06\x66\x61mily\x18\x04 \x02(\t\"\xc5\x01\n\x08\x46pgaInfo\x12\x0f\n\x07\x66pga_id\x18\x01 \x02(\t\x12\x10\n\x08part_num\x18\x02 \x02(\t\x12\x0c\n\x04size\x18\x03 \x02(\t\x12*\n\x05ice40\x18\n \x01(\x0b\x32\x19.apio.proto.Ice40FpgaInfoH\x00\x12(\n\x04\x65\x63p5\x18\x0b \x01(\x0b\x32\x18.apio.proto.Ecp5FpgaInfoH\x00\x12*\n\x05gowin\x18\x0c \x01(\x0b\x32\x19.apio.proto.GowinFpgaInfoH\x00\x42\x06\n\x04\x61rch\"I\n\tVerbosity\x12\x12\n\x03\x61ll\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x14\n\x05synth\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x12\n\x03pnr\x18\x03 \x01(\x08:\x05\x66\x61lse\"e\n\x0b\x45nvrionment\x12\x13\n\x0bplatform_id\x18\x01 \x02(\t\x12\x17\n\x08is_debug\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x12\n\nyosys_path\x18\x03 \x02(\t\x12\x14\n\x0ctrellis_path\x18\x04 \x02(\t\"T\n\x07Project\x12\x10\n\x08\x62oard_id\x18\x01 \x02(\t\x12\x12\n\ntop_module\x18\x02 \x02(\t\x12#\n\x19yosys_synth_extra_options\x18\x03 \x01(\t:\x00\"\x98\x01\n\nLintParams\x12\x14\n\ntop_module\x18\x01 \x01(\t:\x00\x12\x1c\n\rverilator_all\x18\x02 \x01(\x08:\x05\x66\x61lse\x12!\n\x12verilator_no_style\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x12verilator_no_warns\x18\x04 \x03(\t\x12\x17\n\x0fverilator_warns\x18\x05 \x03(\t\"S\n\x0bGraphParams\x12\x30\n\x0boutput_type\x18\x01 \x02(\x0e\x32\x1b.apio.proto.GraphOutputType\x12\x12\n\ntop_module\x18\x02 \x01(\t\"3\n\tSimParams\x12\x13\n\ttestbench\x18\x01 \x01(\t:\x00\x12\x11\n\tforce_sim\x18\x02 \x02(\x08\"%\n\x0e\x41pioTestParams\x12\x13\n\ttestbench\x18\x01 \x01(\t:\x00\"&\n\x0cUploadParams\x12\x16\n\x0eprogrammer_cmd\x18\x01 \x01(\t\"\xe8\x01\n\x0cTargetParams\x12&\n\x04lint\x18\n \x01(\x0b\x32\x16.apio.proto.LintParamsH\x00\x12(\n\x05graph\x18\x0b \x01(\x0b\x32\x17.apio.proto.GraphParamsH\x00\x12$\n\x03sim\x18\x0c \x01(\x0b\x32\x15.apio.proto.SimParamsH\x00\x12*\n\x04test\x18\r \x01(\x0b\x32\x1a.apio.proto.ApioTestParamsH\x00\x12*\n\x06upload\x18\x0e \x01(\x0b\x32\x18.apio.proto.UploadParamsH\x00\x42\x08\n\x06target\"\x95\x02\n\x0bSconsParams\x12\x11\n\ttimestamp\x18\x01 \x02(\t\x12\"\n\x04\x61rch\x18\x02 \x02(\x0e\x32\x14.apio.proto.ApioArch\x12\'\n\tfpga_info\x18\x03 \x02(\x0b\x32\x14.apio.proto.FpgaInfo\x12(\n\tverbosity\x18\x04 \x01(\x0b\x32\x15.apio.proto.Verbosity\x12,\n\x0b\x65nvrionment\x18\x05 \x02(\x0b\x32\x17.apio.proto.Envrionment\x12$\n\x07project\x18\x06 \x02(\x0b\x32\x13.apio.proto.Project\x12(\n\x06target\x18\x07 \x01(\x0b\x32\x18.apio.proto.TargetParams*@\n\x08\x41pioArch\x12\x14\n\x10\x41RCH_UNSPECIFIED\x10\x00\x12\t\n\x05ICE40\x10\x01\x12\x08\n\x04\x45\x43P5\x10\x02\x12\t\n\x05GOWIN\x10\x03*B\n\x0fGraphOutputType\x12\x14\n\x10TYPE_UNSPECIFIED\x10\x00\x12\x07\n\x03SVG\x10\x01\x12\x07\n\x03PNG\x10\x02\x12\x07\n\x03PDF\x10\x03') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'apio_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_APIOARCH']._serialized_start=1514 - _globals['_APIOARCH']._serialized_end=1578 - _globals['_GRAPHOUTPUTTYPE']._serialized_start=1580 - _globals['_GRAPHOUTPUTTYPE']._serialized_end=1646 - _globals['_ICE40FPGAINFO']._serialized_start=26 - _globals['_ICE40FPGAINFO']._serialized_end=69 - _globals['_ECP5FPGAINFO']._serialized_start=71 - _globals['_ECP5FPGAINFO']._serialized_end=128 - _globals['_GOWINFPGAINFO']._serialized_start=130 - _globals['_GOWINFPGAINFO']._serialized_end=161 - _globals['_FPGAINFO']._serialized_start=164 - _globals['_FPGAINFO']._serialized_end=361 - _globals['_VERBOSITY']._serialized_start=363 - _globals['_VERBOSITY']._serialized_end=436 - _globals['_ENVRIONMENT']._serialized_start=438 - _globals['_ENVRIONMENT']._serialized_end=539 - _globals['_PROJECT']._serialized_start=541 - _globals['_PROJECT']._serialized_end=625 - _globals['_LINTPARAMS']._serialized_start=628 - _globals['_LINTPARAMS']._serialized_end=780 - _globals['_GRAPHPARAMS']._serialized_start=782 - _globals['_GRAPHPARAMS']._serialized_end=865 - _globals['_SIMPARAMS']._serialized_start=867 - _globals['_SIMPARAMS']._serialized_end=918 - _globals['_APIOTESTPARAMS']._serialized_start=920 - _globals['_APIOTESTPARAMS']._serialized_end=957 - _globals['_UPLOADPARAMS']._serialized_start=959 - _globals['_UPLOADPARAMS']._serialized_end=997 - _globals['_TARGETPARAMS']._serialized_start=1000 - _globals['_TARGETPARAMS']._serialized_end=1232 - _globals['_SCONSPARAMS']._serialized_start=1235 - _globals['_SCONSPARAMS']._serialized_end=1512 -# @@protoc_insertion_point(module_scope) diff --git a/apio/resources/boards.jsonc b/apio/resources/boards.jsonc index 189a29db..10e2a281 100644 --- a/apio/resources/boards.jsonc +++ b/apio/resources/boards.jsonc @@ -962,4 +962,4 @@ "extra_args": "-c ft2232 -v --file-type bin" } } -} \ No newline at end of file +} diff --git a/apio/scons/README.md b/apio/scons/README.md index 8d10155f..5ea534bf 100644 --- a/apio/scons/README.md +++ b/apio/scons/README.md @@ -16,7 +16,3 @@ export APIO_SCONS_DEBUGGER= set APIO_SCONS_DEBUGGER= ``` -* Print messages with color need to be performed with - secho(...., color=True) otherwise it will be stripped automatically - by click due to the piped output. The the info/warning/error functions - in scons_util.py. diff --git a/apio/scons/SConstruct b/apio/scons/SConstruct index ca011856..b8e5ded9 100644 --- a/apio/scons/SConstruct +++ b/apio/scons/SConstruct @@ -4,11 +4,11 @@ # -- This file is part of the Apio project # -- (C) 2016-2024 FPGAwars # -- Authors Juan Gonzáles, Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 - -from apio.scons.plugin_util import maybe_wait_for_remote_debugger from apio.scons.scons_handler import SconsHandler +from apio.common.common_util import maybe_wait_for_remote_debugger + # -- If system env var APIO_SCONS_DEBUGGER is defined, regardless of its value, # -- we wait on a remote debugger to be attached, e.g. from Visual Studio Code. diff --git a/apio/scons/apio_env.py b/apio/scons/apio_env.py index a279e716..6b6ac754 100644 --- a/apio/scons/apio_env.py +++ b/apio/scons/apio_env.py @@ -2,11 +2,11 @@ # -- This file is part of the Apio project # -- (C) 2016-2018 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """A class with common services for the apio scons handlers. """ @@ -18,24 +18,11 @@ import os from typing import List, Optional -from click import secho from SCons.Script.SConscript import SConsEnvironment from SCons.Environment import BuilderWrapper import SCons.Defaults - -# from apio.scons.apio_args import ApioArgs -from apio.proto.apio_pb2 import SconsParams - - -# -- All the build files and other artifcats are created in this this -# -- subdirectory. -BUILD_DIR = "_build" - -# -- A shortcut with '/' or '\' appended to the build dir name. -BUILD_DIR_SEP = BUILD_DIR + os.sep - -# -- Target name. This is the base file name for various build artifacts. -TARGET = BUILD_DIR_SEP + "hardware" +from apio.common.apio_console import cout +from apio.common.proto.apio_pb2 import SconsParams # pylint: disable=too-many-public-methods @@ -64,19 +51,19 @@ def __init__( ), "DefaultEnvironment already exists" # pylint: enable=protected-access - # -- Determine if we run on windows. Platform id is a required arg. - self.is_windows = ( - "windows" in self.params.envrionment.platform_id.lower() - ) - # Extra info for debugging. if self.is_debug: self.dump_env_vars() + @property + def is_windows(self): + """Returns True if we run on windows.""" + return self.params.environment.is_windows + @property def is_debug(self): """Returns true if we run in debug mode.""" - return self.params.envrionment.is_debug + return self.params.environment.is_debug def targeting(self, *target_names) -> bool: """Returns true if the any of the named target was specified in the @@ -128,8 +115,8 @@ def dump_env_vars(self) -> None: dictionary = self.scons_env.Dictionary() keys = list(dictionary.keys()) keys.sort() - secho() - secho(">>> Env vars BEGIN", fg="magenta", color=True) + cout("") + cout(">>> Env vars BEGIN", style="magenta") for key in keys: - print(f"{key} = {self.scons_env[key]}") - secho("<<< Env vars END\n", fg="magenta", color=True) + cout(f"{key} = {self.scons_env[key]}") + cout("<<< Env vars END\n", style="magenta") diff --git a/apio/scons/plugin_base.py b/apio/scons/plugin_base.py index bd8db851..1b2e2ae0 100644 --- a/apio/scons/plugin_base.py +++ b/apio/scons/plugin_base.py @@ -2,21 +2,22 @@ # -- This file is part of the Apio project # -- (C) 2016-2018 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """Apio scons related utilities..""" from dataclasses import dataclass -from click import secho from SCons.Builder import BuilderBase from SCons.Action import Action from SCons.Script import Builder -from apio.scons.apio_env import ApioEnv, TARGET, BUILD_DIR_SEP -from apio.proto.apio_pb2 import GraphOutputType +from apio.common.apio_console import cout +from apio.scons.apio_env import ApioEnv +from apio.common.apio_consts import TARGET +from apio.common.proto.apio_pb2 import GraphOutputType from apio.scons.plugin_util import ( SRC_SUFFIXES, verilog_src_scanner, @@ -122,11 +123,12 @@ def yosys_dot_builder(self) -> BuilderBase: ) return Builder( + # See https://tinyurl.com/yosys-sv-graph action=( - 'yosys -f verilog -p "show -format dot -colors 1 ' - '-prefix {0}hardware {1}" {2} $SOURCES' + 'yosys -p "read_verilog -sv $SOURCES; show -format dot' + ' -colors 1 -prefix {0} {1}" {2}' ).format( - BUILD_DIR_SEP, + TARGET, top_module, "" if params.verbosity.all else "-q", ), @@ -160,12 +162,7 @@ def graphviz_renderer_builder(self) -> BuilderBase: def completion_action(source, target, env): # noqa """Action function that prints a completion message.""" _ = (source, target, env) # Unused - secho( - f"Generated {TARGET}.{type_str}", - fg="green", - bold=True, - color=True, - ) + cout(f"Generated {TARGET}.{type_str}", style="green", nl="") actions = [ f"dot -T{type_str} $SOURCES -o $TARGET", diff --git a/apio/scons/plugin_ecp5.py b/apio/scons/plugin_ecp5.py index e609cd80..6c1232af 100644 --- a/apio/scons/plugin_ecp5.py +++ b/apio/scons/plugin_ecp5.py @@ -2,11 +2,11 @@ # -- This file is part of the Apio project # -- (C) 2016-2018 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """Apio scons plugin for the ecp5 architecture.""" @@ -16,7 +16,8 @@ from pathlib import Path from SCons.Script import Builder from SCons.Builder import BuilderBase -from apio.scons.apio_env import ApioEnv, TARGET, BUILD_DIR +from apio.scons.apio_env import ApioEnv +from apio.common.apio_consts import TARGET from apio.scons.plugin_base import PluginBase, ArchPluginInfo from apio.scons.plugin_util import ( SRC_SUFFIXES, @@ -38,8 +39,8 @@ def __init__(self, apio_env: ApioEnv): super().__init__(apio_env) # -- Cache values. - trellis_path = Path(apio_env.params.envrionment.trellis_path) - yosys_path = Path(apio_env.params.envrionment.yosys_path) + trellis_path = Path(apio_env.params.environment.trellis_path) + yosys_path = Path(apio_env.params.environment.yosys_path) self.database_path = trellis_path / "database" self.yosys_lib_dir = yosys_path / "ecp5" @@ -111,10 +112,8 @@ def emitter(target, source, env): def bitstream_builder(self) -> BuilderBase: """Creates and returns the bitstream builder.""" return Builder( - action="ecppack --compress --db {0} $SOURCE " - "{1}/hardware.bit".format( + action="ecppack --compress --db {0} $SOURCE $TARGET".format( self.database_path, - BUILD_DIR, ), suffix=".bit", src_suffix=".config", diff --git a/apio/scons/plugin_gowin.py b/apio/scons/plugin_gowin.py index 8e371891..72eb60c7 100644 --- a/apio/scons/plugin_gowin.py +++ b/apio/scons/plugin_gowin.py @@ -2,11 +2,11 @@ # -- This file is part of the Apio project # -- (C) 2016-2018 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """Apio scons plugin for the gowin architecture.""" @@ -16,7 +16,8 @@ from pathlib import Path from SCons.Script import Builder from SCons.Builder import BuilderBase -from apio.scons.apio_env import ApioEnv, TARGET +from apio.scons.apio_env import ApioEnv +from apio.common.apio_consts import TARGET from apio.scons.plugin_base import PluginBase, ArchPluginInfo from apio.scons.plugin_util import ( SRC_SUFFIXES, @@ -38,7 +39,7 @@ def __init__(self, apio_env: ApioEnv): super().__init__(apio_env) # -- Cache values. - yosys_path = Path(apio_env.params.envrionment.yosys_path) + yosys_path = Path(apio_env.params.environment.yosys_path) self.yosys_lib_dir = yosys_path / "gowin" self.yosys_lib_file = yosys_path / "gowin" / "cells_sim.v" diff --git a/apio/scons/plugin_ice40.py b/apio/scons/plugin_ice40.py index fef945ae..caab1366 100644 --- a/apio/scons/plugin_ice40.py +++ b/apio/scons/plugin_ice40.py @@ -2,11 +2,11 @@ # -- This file is part of the Apio project # -- (C) 2016-2018 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """Apio scons plugin for the ice40 architecture.""" @@ -16,7 +16,8 @@ from pathlib import Path from SCons.Script import Builder from SCons.Builder import BuilderBase -from apio.scons.apio_env import ApioEnv, TARGET +from apio.scons.apio_env import ApioEnv +from apio.common.apio_consts import TARGET from apio.scons.plugin_base import PluginBase, ArchPluginInfo from apio.scons.plugin_util import ( SRC_SUFFIXES, @@ -38,7 +39,7 @@ def __init__(self, apio_env: ApioEnv): super().__init__(apio_env) # -- Cache values. - yosys_path = Path(apio_env.params.envrionment.yosys_path) + yosys_path = Path(apio_env.params.environment.yosys_path) self.yosys_lib_dir = yosys_path / "ice40" self.yosys_lib_file = yosys_path / "ice40" / "cells_sim.v" diff --git a/apio/scons/plugin_util.py b/apio/scons/plugin_util.py index 6f9fb5ed..15c3cd08 100644 --- a/apio/scons/plugin_util.py +++ b/apio/scons/plugin_util.py @@ -2,11 +2,11 @@ # -- This file is part of the Apio project # -- (C) 2016-2018 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """Helper functions for apio scons plugins. """ @@ -19,7 +19,8 @@ from dataclasses import dataclass from pathlib import Path from typing import List, Tuple, Dict, Optional, Union -from click import secho, style +from rich.table import Table +from rich import box from SCons import Scanner from SCons.Builder import Builder from SCons.Action import FunctionAction, Action @@ -27,47 +28,14 @@ from SCons.Script.SConscript import SConsEnvironment from SCons.Node import NodeList from SCons.Node.Alias import Alias -import debugpy -from apio.scons.apio_env import ApioEnv, TARGET, BUILD_DIR_SEP +from apio.scons.apio_env import ApioEnv +from apio.common.apio_consts import TARGET, BUILD_DIR +from apio.common.apio_console import cout, cerror, cwarning, cprint # -- A list with the file extensions of the verilog source files. SRC_SUFFIXES = [".v", ".sv"] -TESTBENCH_HINT = "Testbench file names must end with '_tb.v' or '_tb.sv." - - -def secho_lines(colors: List[str], lines: List[str]) -> None: - """Secho list of lines with matching colors. If running out of colors, - repeat the last one..""" - for i, line in enumerate(lines): - fg = colors[i] if i < len(colors) else colors[-1] - secho(line, fg=fg, bold=True, color=True) - - -def maybe_wait_for_remote_debugger(env_var_name: str): - """A rendezvous point for a remote debger. If the environment variable - of given name is set, the function will block until a remote - debugger (e.g. from Visual Studio Code) is attached. - """ - if os.getenv(env_var_name) is not None: - secho(f"Env var '{env_var_name}' was detected.") - port = 5678 - secho(f"Apio SCons for remote debugger on port localhost:{port}.") - debugpy.listen(port) - secho( - "Attach Visual Studio Code python remote python debugger " - f"to port {port}.", - fg="magenta", - color=True, - ) - # -- Block until the debugger connetcs. - debugpy.wait_for_client() - # -- Here the remote debugger is attached and the program continues. - secho( - "Remote debugger is attached, program continues...", - fg="green", - color=True, - ) +TESTBENCH_HINT = "Testbench file names must end with '_tb.v' or '_tb.sv'." def map_params(params: Optional[List[Union[str, Path]]], fmt: str) -> str: @@ -117,22 +85,16 @@ def get_constraint_file( # Case 1: No matching files. if n == 0: result = f"{top_module.lower()}{file_ext}" - secho( - f"Warning: No {file_ext} constraints file, assuming '{result}'.", - fg="yellow", - color=True, - ) + cwarning(f"No {file_ext} constraints file, assuming '{result}'.") return result # Case 2: Exactly one file found. if n == 1: result = str(files[0]) return result # Case 3: Multiple matching files. - secho( - f"Error: Found multiple '*{file_ext}' " - "constrain files, expecting exactly one.", - fg="red", - color=True, + cerror( + f"Found multiple '*{file_ext}' " + "constrain files, expecting exactly one." ) sys.exit(1) @@ -217,9 +179,9 @@ def verilog_src_scanner_func( if Path(dependency).exists(): dependencies.append(dependency) elif apio_env.is_debug: - secho( + cout( f"Dependency candidate {dependency} does not exist, " - "droping." + "dropping." ) # Sort the strings for determinism. @@ -227,9 +189,9 @@ def verilog_src_scanner_func( # Debug info. if apio_env.is_debug: - secho(f"Dependencies of {file_node.name}:", fg="blue", color=True) + cout(f"Dependencies of {file_node.name}:", style="blue") for dependency in dependencies: - secho(f" {dependency}", fg="blue", color=True) + cout(f" {dependency}", style="blue") # All done return apio_env.scons_env.File(dependencies) @@ -343,13 +305,8 @@ def check_valid_testbench_name(testbench: str) -> None: """Check if a testbench name is valid. If not, print an error message and exit.""" if not is_verilog_src(testbench) or not has_testbench_name(testbench): - secho_lines( - ["red"], - [ - f"Error: '{testbench}' is not a valid testbench file name.", - TESTBENCH_HINT, - ], - ) + cerror(f"'{testbench}' is not a valid testbench file name.") + cout(TESTBENCH_HINT, style="yellow") sys.exit(1) @@ -372,32 +329,23 @@ def get_sim_config( elif len(test_srcs) == 0: # -- Case 2 Testbench name was not specified and no testbench files # -- were found in the project. - secho_lines( - ["red"], - [ - "Error: No testbench files found in the project.", - TESTBENCH_HINT, - ], - ) + cerror("No testbench files found in the project.") + cout(TESTBENCH_HINT, style="yellow") + sys.exit(1) elif len(test_srcs) == 1: # -- Case 3 Testbench name was not specified but there is exactly # -- one in the project. testbench = test_srcs[0] - secho_lines( - ["cyan"], - [f"Found testbench file [{testbench}]"], - ) + cout(f"Found testbench file {testbench}", style="cyan") else: # -- Case 4 Testbench name was not specified and there are multiple # -- testbench files in the project. - secho_lines( - ["red", "yellow"], - [ - "Error: Multiple testbench files found in the project.", - "Please specify the testbench file name in the command " - "or in apio.ini 'default-testbench' option.", - ], + cerror("Multiple testbench files found in the project.") + cout( + "Please specify the testbench file name in the command " + "or in apio.ini 'default-testbench' option.", + style="yellow", ) sys.exit(1) @@ -407,7 +355,7 @@ def get_sim_config( # -- Construct a SimulationParams with all the synth files + the # -- testbench file. testbench_name = basename(testbench) - build_testbench_name = BUILD_DIR_SEP + testbench_name + build_testbench_name = str(BUILD_DIR / testbench_name) srcs = synth_srcs + [testbench] return SimulationConfig(testbench_name, build_testbench_name, srcs) @@ -434,13 +382,8 @@ def get_tests_configs( elif len(test_srcs) == 0: # -- Case 2 - Testbench file name was not specified and there are no # -- testbench files in the project. - secho_lines( - ["red", "yellow"], - [ - "Error: No testbench files found in the project.", - TESTBENCH_HINT, - ], - ) + cerror("No testbench files found in the project.") + cout(TESTBENCH_HINT, style="yellow") sys.exit(1) else: # -- Case 3 - Testbench file name was not specified but there are one @@ -454,7 +397,7 @@ def get_tests_configs( configs = [] for tb in testbenches: testbench_name = basename(tb) - build_testbench_name = BUILD_DIR_SEP + testbench_name + build_testbench_name = str(BUILD_DIR / testbench_name) srcs = synth_srcs + [tb] configs.append( SimulationConfig(testbench_name, build_testbench_name, srcs) @@ -505,18 +448,16 @@ def report_source_files_issues( continue # -- Here the file is a testbench file. - secho(f"Testbench {file.name}", fg="cyan", bold=True, color=True) + cout(f"Testbench {file.name}", style="cyan") # -- Read the testbench file text. file_text = file.get_text_contents() # -- if contains $dumpfile, print a warning. if testbench_dumpfile_re.findall(file_text): - secho( - f"Warning: [{file.name}] Using $dumpfile() in apio " - "testbenches is not recomanded.", - fg="magenta", - color=True, + cwarning( + "Using $dumpfile() in apio " + "testbenches is not recomanded." ) return Action(report_source_files_issues, "Scanning for issues.") @@ -530,11 +471,9 @@ def source_files(apio_env: ApioEnv) -> Tuple[List[str], List[str]]: # -- Get a list of all *.v and .sv files in the project dir. files: List[File] = apio_env.scons_env.Glob("*.sv") if files: - secho( - "Warning: project contains .sv files, system-verilog support " - "is experimental.", - fg="yellow", - color=True, + cwarning( + "Project contains .sv files, system-verilog support " + "is experimental." ) files = files + apio_env.scons_env.Glob("*.v") @@ -549,7 +488,83 @@ def source_files(apio_env: ApioEnv) -> Tuple[List[str], List[str]]: return (synth_srcs, test_srcs) -# pylint: disable=too-many-locals +# R0801: Similar lines in 2 files +# pylint: disable=R0801 +def _print_pnr_utilization_report(report: Dict[str, any]): + table = Table( + show_header=True, + show_lines=False, + box=box.SQUARE, + border_style="dim", + title="FPGA Resource Utilization", + title_justify="left", + padding=(0, 2), + ) + + # -- Add columns. + table.add_column("RESOURCE", no_wrap=True) + table.add_column("USED", no_wrap=True, justify="right") + table.add_column("TOTAL", no_wrap=True, justify="right") + table.add_column("UTIL.", no_wrap=True, justify="right") + + # -- Add rows + utilization = report["utilization"] + for resource, vals in utilization.items(): + used = vals["used"] + used_str = f"{used} " if used else "" + available = vals["available"] + available_str = f"{available} " + percents = int(100 * used / available) + percents_str = f"{percents}% " if used else "" + style = "magenta" if used > 0 else None + table.add_row( + resource, used_str, available_str, percents_str, style=style + ) + + # -- Render the table + cout() + cprint(table) + + +def _maybe_print_pnr_clocks_report( + report: Dict[str, any], clk_name_index: int +) -> bool: + clocks = report["fmax"] + if len(clocks) == 0: + return False + + table = Table( + show_header=True, + show_lines=True, + box=box.SQUARE, + border_style="dim", + title="Clock Information", + title_justify="left", + padding=(0, 2), + ) + + # -- Add columns + table.add_column("CLOCK", no_wrap=True) + table.add_column( + "MAX SPEED [Mhz]", no_wrap=True, justify="right", style="magenta" + ) + + # -- Add rows. + clocks = report["fmax"] + for clk_net, vals in clocks.items(): + # -- Extract clock name from the net name. + clk_signal = clk_net.split("$")[clk_name_index] + # -- Extract speed + max_mhz = vals["achieved"] + # -- Add row. + table.add_row(clk_signal, f"{max_mhz:.2f}") + + # -- Render the table + cout() + cprint(table) + return True + + def _print_pnr_report( json_txt: str, clk_name_index: int, @@ -557,50 +572,26 @@ def _print_pnr_report( ) -> None: """Accepts the text of the pnr json report and prints it in a user friendly way. Used by the 'apio report' command.""" - # -- Json text to tree of Dicts. + # -- Parse the json text into a tree of dicts. report: Dict[str, any] = json.loads(json_txt) - # --- Report utilization - secho("") - secho("UTILIZATION:", fg="cyan", bold=True, color=True) - utilization = report["utilization"] - for resource, vals in utilization.items(): - available = vals["available"] - used = vals["used"] - percents = int(100 * used / available) - fg = "magenta" if used > 0 else None - secho( - f"{resource:>20}: {used:5} {available:5} {percents:5}%", - fg=fg, - color=True, - ) + # -- Print the utilization table. + _print_pnr_utilization_report(report) - # -- Report max clock speeds. - # -- - # -- NOTE: As of Oct 2024, some projects do not generate timing - # -- information and this is being investigated. - # -- See https://github.com/FPGAwars/icestudio/issues/774 for details. - secho("") - secho("CLOCKS:", fg="cyan", bold=True, color=True) - clocks = report["fmax"] - if len(clocks) > 0: - for clk_net, vals in clocks.items(): - # -- Extract clock name from the net name. - clk_signal = clk_net.split("$")[clk_name_index] - - # -- Report speed. - max_mhz = vals["achieved"] - styled_max_mhz = style(f"{max_mhz:7.2f}", fg="magenta") - secho(f"{clk_signal:>20}: {styled_max_mhz} Mhz max") - - # -- For now we ignore the critical path report in the pnr report and - # -- refer the user to the pnr verbose output. - secho("") + # -- Print the optional clocks table. + clock_report_printed = _maybe_print_pnr_clocks_report( + report, clk_name_index + ) + + # -- Print summary. + cout("") + if not clock_report_printed: + cout("No clocks were found in the design.", style="yellow") if not verbose: - secho( - "Use 'apio report --verbose' for more details.", - fg="yellow", - color=True, + cout( + "Run 'apio report --verbose' for more details.", + nl=False, + style="yellow", ) @@ -640,11 +631,9 @@ def get_programmer_cmd(apio_env: ApioEnv) -> str: # It's an error if the programmer command doesn't have the $SOURCE # placeholder when scons inserts the binary file name. if "$SOURCE" not in programmer_cmd: - secho( - "Error: [Internal] $SOURCE is missing in programmer command: " - f"{programmer_cmd}", - fg="red", - color=True, + cerror( + "[Internal] $SOURCE is missing in programmer command: " + f"{programmer_cmd}" ) sys.exit(1) @@ -758,10 +747,10 @@ def configure_cleanup(apio_env: ApioEnv) -> None: # -- Get the list of all files to clean. Scons adds to the list non # -- existing files from other targets it encountered. files_to_clean = ( - scons_env.Glob(f"{BUILD_DIR_SEP}*") + scons_env.Glob(str(BUILD_DIR / "*")) + scons_env.Glob("zadig.ini") + scons_env.Glob(".sconsign.dblite") - + scons_env.Glob("_build") + + scons_env.Glob(str(BUILD_DIR)) ) # pylint: disable=fixme @@ -779,11 +768,7 @@ def configure_cleanup(apio_env: ApioEnv) -> None: ) if legacy_files_to_clean: - secho( - "Deleting also leftover files.", - fg="yellow", - color=True, - ) + cwarning("Deleting also leftover files.") files_to_clean.extend(legacy_files_to_clean) diff --git a/apio/scons/scons_handler.py b/apio/scons/scons_handler.py index 9a3c5d40..ff9f786f 100644 --- a/apio/scons/scons_handler.py +++ b/apio/scons/scons_handler.py @@ -2,24 +2,27 @@ # -- This file is part of the Apio project # -- (C) 2016-2018 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """Apio scons related utilities..""" import sys -from click import secho from SCons.Script import ARGUMENTS, COMMAND_LINE_TARGETS from google.protobuf import text_format from apio.scons.plugin_ice40 import PluginIce40 from apio.scons.plugin_ecp5 import PluginEcp5 from apio.scons.plugin_gowin import PluginGowin -from apio.proto.apio_pb2 import SconsParams, ICE40, ECP5, GOWIN -from apio.scons.apio_env import ApioEnv, TARGET +from apio.common.proto.apio_pb2 import SconsParams, ICE40, ECP5, GOWIN +from apio.common import apio_console +from apio.common.apio_consts import BUILD_DIR +from apio.scons.apio_env import ApioEnv +from apio.common.apio_consts import TARGET from apio.scons.plugin_base import PluginBase +from apio.common import rich_lib_windows from apio.scons.plugin_util import ( get_sim_config, get_tests_configs, @@ -29,6 +32,7 @@ get_programmer_cmd, configure_cleanup, ) +from apio.common.apio_console import cerror, cout # -- Scons builders ids. SYNTH_BUILDER = "SYNTH_BUILDER" @@ -56,7 +60,7 @@ def start() -> None: execute an SconsHandler.""" # -- Read the text of the scons params file. - with open("_build/scons.params", "r", encoding="utf8") as f: + with open(BUILD_DIR / "scons.params", "r", encoding="utf8") as f: proto_text = f.read() # -- Parse the text into SconsParams object. @@ -66,6 +70,16 @@ def start() -> None: timestamp = ARGUMENTS["timestamp"] assert params.timestamp == timestamp + # -- If running on windows, apply the lib library workaround + if params.environment.is_windows: + assert params.HasField("rich_lib_windows_params"), params + rich_lib_windows.apply_workaround(params.rich_lib_windows_params) + else: + assert not params.HasField("rich_lib_windows_params"), params + + # -- Force colors even though we are piped out. + apio_console.configure(force_terminal=True) + # -- Create the apio environment. apio_env = ApioEnv(COMMAND_LINE_TARGETS, params) @@ -77,7 +91,7 @@ def start() -> None: elif params.arch == GOWIN: plugin = PluginGowin(apio_env) else: - print( + cout( f"Apio SConstruct dispatch error: unknown arch [{params.arch}]" ) sys.exit(1) @@ -426,11 +440,8 @@ def execute(self): self._register_lint_target(synth_srcs, test_srcs) else: - secho( - f"Error: scons handler got unexpected target [{target}]", - fg="red", - ) + cerror(f"Unexpected scons target: {target}") sys.exit(1) - # -- Note that we just registered builders and target. The actual - # -- execution is done by scons once this method returns. + # -- Note that so far we just registered builders and target. + # -- The actual execution is done by scons once this method returns. diff --git a/apio/utils/cmd_util.py b/apio/utils/cmd_util.py index 9f25985c..85b16365 100644 --- a/apio/utils/cmd_util.py +++ b/apio/utils/cmd_util.py @@ -2,20 +2,27 @@ # -- This file is part of the Apio project # -- (C) 2016-2018 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """Utility functionality for apio click commands. """ import sys from dataclasses import dataclass from typing import List, Dict, Union import click -from click import secho +from click.formatting import HelpFormatter +from apio.common.apio_console import ( + HELP_SUBCOMMANDS, + ConsoleCapture, + cout, + cerror, + cstyle, + docs_text, +) from apio.utils import util -from apio.profile import Profile def fatal_usage_error(cmd_ctx: click.Context, msg: str) -> None: @@ -24,15 +31,15 @@ def fatal_usage_error(cmd_ctx: click.Context, msg: str) -> None: cmd_ctx: The context that was passed to the command. msg: A single line short error message. """ - # Mimiking the usage error message from click/exceptions.py. + # Mimicking the usage error message from click/exceptions.py. # E.g. "Try 'apio packages -h' for help." - secho(cmd_ctx.get_usage()) - secho( + cout(cmd_ctx.get_usage()) + cout( f"Try '{cmd_ctx.command_path} {cmd_ctx.help_option_names[0]}' " "for help." ) - secho() - secho(f"Error: {msg}", fg="red") + cout("") + cerror(f"{msg}") sys.exit(1) @@ -54,7 +61,7 @@ def _params_ids_to_aliases( cmd_ctx: click.Context, params_ids: List[str] ) -> List[str]: """Maps param ids to their respective user facing canonical aliases. - The order of the params is in the inptut list is preserved. + The order of the params is in the input list is preserved. For the definition of param ids see check_exclusive_params(). @@ -222,9 +229,27 @@ class ApioSubgroup: commands: List[click.Command] +def _format_apio_markdown_help_text( + markdown_text: str, formatter: HelpFormatter +) -> None: + """Format command's or group's help markdown text into a given + click formatter.""" + + # -- Style the metadata text. + styled_text = None + with ConsoleCapture() as capture: + docs_text(markdown_text.rstrip("\n"), end="") + styled_text = capture.value + + # -- Raw write to the output, with indent. + lines = styled_text.split("\n") + for line in lines: + formatter.write((" " + line).rstrip(" ") + "\n") + + class ApioGroup(click.Group): - """A customized click.Group class that allow to group subcommand by - categories.""" + """A customized click.Group class that allows apio customized help + format.""" def __init__(self, *args, **kwargs): # -- Consume the 'subgroups' arg. @@ -242,26 +267,26 @@ def __init__(self, *args, **kwargs): self.add_command(cmd=cmd, name=cmd.name) # @override - def get_help(self, ctx: click.Context) -> str: - """Formats the help into a string and returns it. We override the - base class method to list the subcommands by categories. - """ - - # -- Apply the color prefernece. This is required because the -h - # -- options bypasses the command handler so we don't get to create - # -- an apio context. - Profile.apply_color_preferences() + def format_help_text( + self, ctx: click.Context, formatter: HelpFormatter + ) -> None: + """Overrides the parent method that formats the command's help text.""" + _format_apio_markdown_help_text(self.help, formatter) - # -- Get the default help text for this command. - original_help = super().get_help(ctx) + # @override + def format_options( + self, ctx: click.Context, formatter: HelpFormatter + ) -> None: + """Overrides the parent method which formats the options and sub + commands.""" - # -- The auto generated click help lines (apio --help) - help_lines = original_help.split("\n") + # -- Call the grandparent method which formats the options without + # -- the subcommands. + click.Command.format_options(self, ctx, formatter) - # -- Extract the header of the text help. We will generate ourselves - # -- and append the command list. - index = help_lines.index("Commands:") - result_lines = help_lines[:index] + # -- Format the subcommands, grouped by the apio defined subgroups + # -- in self._subgroups. + formatter.write("\n") # -- Get a flat list of all subcommand names. cmd_names = [ @@ -270,20 +295,44 @@ def get_help(self, ctx: click.Context) -> str: for cmd in subgroup.commands ] - # -- Find the length of the longerst name. + # -- Find the length of the longest name. max_name_len = max(len(name) for name in cmd_names) # -- Generate the subcommands short help, grouped by subgroup. for subgroup in self._subgroups: - result_lines.append(f"{subgroup.title}:") + assert isinstance(subgroup, ApioSubgroup), subgroup + formatter.write(f"{subgroup.title}:\n") + # -- Print the commands that are in this subgroup. for cmd in subgroup.commands: # -- We pad for field width and then apply color. - styled_name = click.style( - f"{cmd.name:{max_name_len}}", fg="magenta" + styled_name = cstyle( + f"{cmd.name:{max_name_len}}", style=HELP_SUBCOMMANDS ) - result_lines.append( - f" {ctx.command_path} {styled_name} {cmd.short_help}" + formatter.write( + f" {ctx.command_path} {styled_name} {cmd.short_help}\n" ) - result_lines.append("") + formatter.write("\n") - return "\n".join(result_lines) + # @override + def get_help(self, ctx: click.Context) -> str: + """Overrides a super method to add blank line at the end of the help + text.""" + return super().get_help(ctx) + "\n" + + +class ApioCommand(click.Command): + """A customized click.Command class that allows apio customized help + format.""" + + # @override + def format_help_text( + self, ctx: click.Context, formatter: HelpFormatter + ) -> None: + """Overrides the parent method that formats the command's help text.""" + _format_apio_markdown_help_text(self.help, formatter) + + # @override + def get_help(self, ctx: click.Context) -> str: + """Overrides a super method to add blank line at the end of the help + text.""" + return super().get_help(ctx) + "\n" diff --git a/apio/utils/env_options.py b/apio/utils/env_options.py index 86c54c3f..781a288f 100644 --- a/apio/utils/env_options.py +++ b/apio/utils/env_options.py @@ -2,11 +2,11 @@ # -- This file is part of the Apio project # -- (C) 2016-2018 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """Functions for reading the APIO env options. This are system env variables that are used to modify the default behavior of APIO. """ @@ -28,8 +28,8 @@ # -- Env variable to enable printout of additional information for debugging. # -- It is not intended to change the logic of apio but just to provide # -- additional information about its internal behavior. Currently -# -- it's used as a binary flag with existance indicating True and non -# -- existance indicating False. +# -- it's used as a binary flag with existence indicating True and non +# -- existence indicating False. # -- # -- Do not access it directly. For the apio process use util.is_debug() and # -- for the scons process use scons_util.is_debug(). @@ -59,7 +59,7 @@ def get(var_name: str, default: str = None): var_value = os.getenv(var_name) if var_value is None: - # -- Var is undefied. Use default + # -- Var is undefined. Use default var_value = default else: # -- Var is defined. For windows benefit, remove optional quotes. diff --git a/apio/utils/jsonc.py b/apio/utils/jsonc.py index 2e46c766..8770261d 100644 --- a/apio/utils/jsonc.py +++ b/apio/utils/jsonc.py @@ -2,11 +2,11 @@ # -- This file is part of the Apio project # -- (C) 2016-2024 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """A simple utility to string '//' comments from a json file. @@ -80,11 +80,11 @@ class _Transition: def to_json(text: str) -> str: """Convert jasonc input to json by removing '//' comments. Line and number and characters position are preserved to any later json parsing - errors meanigful to the user. + errors meaningful to the user. """ output = [] - # -- Indicates the input position that is aleardy covered by the content + # -- Indicates the input position that is already covered by the content # -- in output. It can be larger than the size of the text in output since # -- we drop comment text. output_pos = 0 @@ -137,5 +137,5 @@ def to_json(text: str) -> str: if state != _State.IN_COMMENT: output.append(text[output_pos:]) - # -- Concatanate the text pieces into a string. + # -- Concatenates the text pieces into a string. return "".join(output) diff --git a/apio/utils/pkg_util.py b/apio/utils/pkg_util.py index bb3abc35..f0bb3c4d 100644 --- a/apio/utils/pkg_util.py +++ b/apio/utils/pkg_util.py @@ -2,19 +2,18 @@ # -- This file is part of the Apio project # -- (C) 2016-2018 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """Utility functions related to apio packages.""" from typing import List, Callable, Tuple from pathlib import Path from dataclasses import dataclass import os -import click -from click import secho +from apio.common.apio_console import cout, cstyle from apio.apio_context import ApioContext from apio.utils import util @@ -68,24 +67,24 @@ def _dump_env_mutations( apio_ctx: ApioContext, mutations: EnvMutations ) -> None: """Dumps a user friendly representation of the env mutations.""" - secho("Envirnment settings:", fg="magenta") + cout("Environment settings:", style="magenta") # -- Print PATH mutations. - windows = apio_ctx.is_windows() + windows = apio_ctx.is_windows for p in reversed(mutations.paths): - styled_name = click.style("PATH", fg="magenta") + styled_name = cstyle("PATH", style="magenta") if windows: - secho(f"set {styled_name}={p};%PATH%") + cout(f"set {styled_name}={p};%PATH%") else: - secho(f'{styled_name}="{p}:$PATH"') + cout(f'{styled_name}="{p}:$PATH"') # -- Print vars mutations. for name, val in mutations.vars: - styled_name = click.style(name, fg="magenta") + styled_name = cstyle(name, style="magenta") if windows: - secho(f"set {styled_name}={val}") + cout(f"set {styled_name}={val}") else: - secho(f'{styled_name}="{val}"') + cout(f'{styled_name}="{val}"') def _apply_env_mutations(mutations: EnvMutations) -> None: @@ -120,7 +119,7 @@ def set_env_for_packages( If quite is set, no output is printed. When verbose is set, additional output such as the env vars mutations are printed, otherwise, a minimal information is printed to make the user aware that they commands they - see are exectuted in a modified env settings. + see are executed in a modified env settings. """ # -- If this fails, this is a programming error. Quiet and verbose @@ -136,13 +135,13 @@ def set_env_for_packages( # -- If this is the first call in this apio invocation, apply the # -- mutations. These mutations are temporary for the lifetime of this # -- process and does not affect the user's shell environment. - # -- The mutations are also inheritated by child processes such as the + # -- The mutations are also inherited by child processes such as the # -- scons processes. if not apio_ctx.env_was_already_set: _apply_env_mutations(mutations) apio_ctx.env_was_already_set = True if not verbose and not quiet: - secho("Setting the envinronment.") + cout("Setting the environment.") @dataclass @@ -150,12 +149,12 @@ class PackageScanResults: """Represents results of packages scan.""" # -- Normal and Error. Packages in platform_packages that are installed - # -- regardless if the versin matches or not. - installed_package_names: List[str] - # -- Error. The subset of installed_package_names that have version - # -- mismatch. - bad_version_package_names_subset: List[str] - # -- Normal. Packages in platform_packages that are uninstaleld properly. + # -- regardless if the version matches or not. + installed_ok_package_names: List[str] + # -- Error. Packages in platform_packages that are installed but with + # -- version mismatch. + bad_version_package_names: List[str] + # -- Normal. Packages in platform_packages that are uninstalled properly. uninstalled_package_names: List[str] # -- Error. Packages in platform_packages with broken installation. E.g, # -- registered in profile but package directory is missing. @@ -164,17 +163,26 @@ class PackageScanResults: # -- in platform_packages. orphan_package_names: List[str] # -- Error. Basenames of directories in packages dir that don't match - # -- folder_name of packages in platform_packates. + # -- folder_name of packages in platform_packages. orphan_dir_names: List[str] # -- Error. Basenames of all files in packages directory. That directory is # -- expected to contain only directories for packages.a orphan_file_names: List[str] + def packages_installed_ok(self) -> bool: + """Returns true if all packages are installed ok, regardless of + other fixable errors.""" + return ( + len(self.bad_version_package_names) == 0 + and len(self.uninstalled_package_names) == 0 + and len(self.broken_package_names) == 0 + ) + def num_errors_to_fix(self) -> bool: """Returns the number of errors that required , having a non installed packages is not considered an error that need to be fix.""" return ( - len(self.bad_version_package_names_subset) + len(self.bad_version_package_names) + len(self.broken_package_names) + len(self.orphan_package_names) + len(self.orphan_dir_names) @@ -190,14 +198,14 @@ def is_all_ok(self) -> bool: def dump(self): """Dump the content of this object. For debugging.""" - print("Package scan results:") - print(f" Installed {self.installed_package_names}") - print(f" bad version {self.bad_version_package_names_subset}") - print(f" Uninstalled {self.uninstalled_package_names}") - print(f" Broken {self.broken_package_names}") - print(f" Orphan ids {self.orphan_package_names}") - print(f" Orphan dirs {self.orphan_dir_names}") - print(f" Orphan files {self.orphan_file_names}") + cout("Package scan results:") + cout(f" Installed {self.installed_ok_package_names}") + cout(f" bad version {self.bad_version_package_names}") + cout(f" Uninstalled {self.uninstalled_package_names}") + cout(f" Broken {self.broken_package_names}") + cout(f" Orphan ids {self.orphan_package_names}") + cout(f" Orphan dirs {self.orphan_dir_names}") + cout(f" Orphan files {self.orphan_file_names}") def package_version_ok( @@ -207,8 +215,8 @@ def package_version_ok( cached_config_ok: bool, verbose: bool, ) -> bool: - """Return true if the packagea is both in profile and plagrom packages - and its version in the provile meet the requirements in the + """Return true if the package is both in profile and platform packages + and its version in the profile meet the requirements in the config.jsonc file. Otherwise return false.""" # If this package is not applicable to this platform, return False. @@ -227,11 +235,12 @@ def package_version_ok( package_name, cached_config_ok=cached_config_ok, verbose=verbose ) - # -- Compare. We expect the two version to be nomalized and ths a string + # -- Compare. We expect the two version to be normalized and ths a string # -- comparison is sufficient. return current_ver == remote_ver +# pylint: disable=too-many-branches def scan_packages( apio_ctx: ApioContext, *, cached_config_ok: bool, verbose: bool ) -> PackageScanResults: @@ -251,7 +260,7 @@ def scan_packages( # -- Collect package's folder names in a set. For a later use. platform_folder_names.add(package_name) - # -- Classify the package as one of three cases. + # -- Classify the package as one of four cases. in_profile = package_name in apio_ctx.profile.packages has_dir = apio_ctx.get_package_dir(package_name).is_dir() version_ok = package_version_ok( @@ -261,17 +270,20 @@ def scan_packages( verbose=verbose, ) if in_profile and has_dir: - result.installed_package_names.append(package_name) - if not version_ok: - # -- The subset of installed_package_namess that has bad - # -- version. - result.bad_version_package_names_subset.append(package_name) + if version_ok: + # Case 1: Package installed ok. + result.installed_ok_package_names.append(package_name) + else: + # -- Case 2: Package installed but version mismatch. + result.bad_version_package_names.append(package_name) elif not in_profile and not has_dir: + # -- Case 3: Package not installed. result.uninstalled_package_names.append(package_name) else: + # -- Case 4: Package is broken. result.broken_package_names.append(package_name) - # -- Scan the packagtes ids that are registered in profile as installed + # -- Scan the packages ids that are registered in profile as installed # -- the ones that are not platform_packages as orphans. for package_name in apio_ctx.profile.packages: if package_name not in apio_ctx.platform_packages: @@ -291,86 +303,3 @@ def scan_packages( result.dump() return result - - -def _list_section(title: str, items: List[List[str]], color: str) -> None: - """A helper function for printing one serction of list_packages().""" - # -- Construct horizontal lines at terminal width. - config = util.get_terminal_config() - line_width = config.terminal_width if config.terminal_mode else 80 - line = "─" * line_width - dline = "═" * line_width - - # -- Print the section. - secho() - secho(dline, fg=color) - secho(title, fg=color, bold=True) - for item in items: - secho(line) - for sub_item in item: - secho(sub_item) - secho(dline, fg=color) - - -# pylint: disable=too-many-branches -def list_packages(apio_ctx: ApioContext, scan: PackageScanResults) -> None: - """Prints in a user friendly format the results of a packages scan.""" - - # -- Shortcuts to reduce clutter. - get_package_version = apio_ctx.profile.get_package_installed_version - get_package_info = apio_ctx.get_package_info - - # --Print the installed packages, if any. - if scan.installed_package_names: - items = [] - for package_name in scan.installed_package_names: - name = click.style(f"{package_name}", fg="cyan", bold=True) - version = get_package_version(package_name) - if package_name in scan.bad_version_package_names_subset: - note = click.style(" [Wrong version]", fg="red", bold=True) - else: - note = "" - description = get_package_info(package_name)["description"] - items.append([f"{name} {version}{note}", f"{description}"]) - _list_section("Installed packages:", items, "green") - - # -- Print the uninstalled packages, if any, - if scan.uninstalled_package_names: - items = [] - for package_name in scan.uninstalled_package_names: - name = click.style(f"{package_name}", fg="cyan", bold=True) - description = get_package_info(package_name)["description"] - items.append([f"{name} {description}"]) - _list_section("Uinstalled packages:", items, "yellow") - - # -- Print the broken packages, if any, - if scan.broken_package_names: - items = [] - for package_name in scan.broken_package_names: - name = click.style(f"{package_name}", fg="red", bold=True) - description = get_package_info(package_name)["description"] - items.append([f"{name} {description}"]) - _list_section("[Error] Broken packages:", items, None) - - # -- Print the orphan packages, if any, - if scan.orphan_package_names: - items = [] - for package_name in scan.orphan_package_names: - name = click.style(f"{package_name}", fg="red", bold=True) - items.append([name]) - _list_section("[Error] Unknown packages:", items, None) - - # -- Print orphan directories and files, if any, - if scan.orphan_dir_names or scan.orphan_file_names: - items = [] - for name in sorted(scan.orphan_dir_names + scan.orphan_file_names): - name = click.style(f"{name}", fg="red", bold=True) - items.append([name]) - _list_section("[Error] Unknown files and directories:", items, None) - - # -- Print an error summary - if scan.num_errors_to_fix(): - secho(f"Total of {util.plurality(scan.num_errors_to_fix(), 'error')}") - - # -- A line seperator. For asthetic reasons. - secho() diff --git a/apio/utils/util.py b/apio/utils/util.py index 7893c32d..5086d627 100644 --- a/apio/utils/util.py +++ b/apio/utils/util.py @@ -2,11 +2,11 @@ # -- This file is part of the Apio project # -- (C) 2016-2018 FPGAwars # -- Author Jesús Arroyo -# -- Licence GPLv2 +# -- License GPLv2 # -- Derived from: # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets -# ---- Licence Apache v2 +# ---- License Apache v2 """Misc utility functions and classes.""" import sys @@ -15,18 +15,16 @@ import traceback import importlib.metadata from functools import wraps -import shutil from enum import Enum from dataclasses import dataclass from typing import Optional, Any, Tuple, List import subprocess from threading import Thread from pathlib import Path -from click import secho from varname import argname from serial.tools.list_ports import comports -import requests from apio.utils import env_options +from apio.common.apio_console import cout, cerror # ---------------------------------------- # -- Constants @@ -89,45 +87,6 @@ class TerminalMode(Enum): PIPE = 2 -@dataclass(frozen=True) -class TerminalConfig: - """Contains the stdout/err terminal/pipe configuration.""" - - mode: TerminalMode # TERMINAL or PIPE. - terminal_width: Optional[int] # Terminal width. None in PIPE mode. - - def __post_init__(self): - """Validates initialization.""" - assert isinstance(self.mode, TerminalMode), self - assert (self.terminal_width is not None) == self.terminal_mode, self - - @property - def terminal_mode(self) -> bool: - """True iff in terminal mode.""" - return self.mode == TerminalMode.TERMINAL - - @property - def pipe_mode(self) -> bool: - """True iff in pipe mode.""" - return self.mode == TerminalMode.PIPE - - -def get_terminal_config() -> TerminalConfig: - """Return the terminal configuration of of the current process.""" - - # Try to get terminal width, with a fallback default if not a terminal. - terminal_width, _ = shutil.get_terminal_size(fallback=(999, 999)) - - # We got the fallback width so assuming a pipe. - if terminal_width == 999: - return TerminalConfig(mode=TerminalMode.PIPE, terminal_width=None) - - # We got an actual terminal width so assuming a terminal. - return TerminalConfig( - mode=TerminalMode.TERMINAL, terminal_width=terminal_width - ) - - def get_path_in_apio_package(subpath: str) -> Path: """Get the full path to the given folder in the apio package. Inputs: @@ -218,12 +177,12 @@ def exec_command(*args, **kwargs) -> CommandResult: # -- User has pressed the Ctrl-C for aborting the command except KeyboardInterrupt: - secho("Aborted by user", fg="red") + cerror("Aborted by user") sys.exit(1) # -- The command does not exist! except FileNotFoundError: - secho(f"Command not found:\n{args}", fg="red") + cerror("Command not found:", args) sys.exit(1) # -- If stdout pipe is an AsyncPipe, extract its text. @@ -245,64 +204,6 @@ def exec_command(*args, **kwargs) -> CommandResult: return result -def get_pypi_latest_version() -> str: - """Get the latest stable version of apio from Pypi - Internet connection is required - Returns: A string with the version (Ex: "0.9.0") - In case of error, it returns None - """ - - # -- Error message common to all exceptions - error_msg = "Error: could not connect to Pypi\n" - - # -- Read the latest apio version from pypi - # -- More information: https://warehouse.pypa.io/api-reference/json.html - try: - req = requests.get( - "https://pypi.python.org/pypi/apio/json", timeout=10 - ) - req.raise_for_status() - - # -- Connection error - except requests.exceptions.ConnectionError as e: - secho( - f"\n{error_msg}" "Check your internet connection and try again\n", - fg="red", - ) - print_exception_developers(e) - return None - - # -- HTTP Error - except requests.exceptions.HTTPError as e: - secho(f"\nHTTP ERROR\n{error_msg}", fg="red") - print_exception_developers(e) - return None - - # -- Timeout! - except requests.exceptions.Timeout as e: - secho(f"\nTIMEOUT!\n{error_msg}", fg="red") - print_exception_developers(e) - return None - - # -- Another error - except requests.exceptions.RequestException as e: - secho(f"\nFATAL ERROR!\n{error_msg}", fg="red") - print_exception_developers(e) - return None - - # -- Get the version field from the json response - version = req.json()["info"]["version"] - - return version - - -def print_exception_developers(e): - """Print a message for developers, caused by the exception e""" - - secho("Info for developers:") - secho(f"{e}\n", fg="yellow") - - def resolve_project_dir( project_dir_arg: Optional[Path], *, @@ -326,10 +227,7 @@ def resolve_project_dir( # -- Make sure the folder doesn't exist as a file. if project_dir.is_file(): - secho( - f"Error: project directory is already a file: {project_dir}", - fg="red", - ) + cerror(f"Project directory is a file: {project_dir}") sys.exit(1) # -- If the folder exists we are good @@ -338,15 +236,12 @@ def resolve_project_dir( # -- Here when dir doesn't exist. Fatal error if must exist. if must_exist: - secho( - f"Error: project directory is missing: {str(project_dir)}", - fg="red", - ) + cerror(f"Project directory is missing: {str(project_dir)}") sys.exit(1) # -- Create the directory if requested. if create_if_missing: - secho(f"Creating folder: {project_dir}") + cout(f"Creating folder: {project_dir}") project_dir.mkdir(parents=True) # -- All done @@ -420,21 +315,21 @@ def get_tinyprog_meta() -> list: ]' """ - # -- Construct the command to execute. Since we exectute tinyprog from + # -- Construct the command to execute. Since we execute tinyprog from # -- the apio packages which add to the path, we can use a simple name. command = ["tinyprog", "--pyserial", "--meta"] command_str = " ".join(command) # -- Execute the command! # -- It should return the meta information as a json string - secho(command_str) + cout(command_str) result = exec_command(command) if result.exit_code != 0: - secho( - f"Warning: the command `{command_str}`failed with exit code " + cout( + f"Warning: the command '{command_str}' failed with exit code " f"{result.exit_code}", - color="yellow", + style="yellow", ) return [] @@ -443,11 +338,11 @@ def get_tinyprog_meta() -> list: meta = json.loads(result.out_text) except json.decoder.JSONDecodeError as exc: - secho( - f"Warning: invalid json dnvalid data provided by `{command_str}`", - fg="yellow", + cout( + f"Warning: invalid json data provided by `{command_str}`", + style="yellow", ) - secho(f"{exc}", fg="red") + cout(f"{exc}", style="red") return [] # -- Return the meta-data @@ -497,7 +392,7 @@ def plurality(obj: Any, singular: str, plural: str = None) -> str: else: n = len(obj) - # -- For value of 1 return the signgular form. + # -- For value of 1 return the singular form. if n == 1: return f"{n} {singular}" @@ -510,7 +405,7 @@ def plurality(obj: Any, singular: str, plural: str = None) -> str: def list_plurality(str_list: List[str], conjunction: str) -> str: """Format a list as a human friendly string.""" # -- This is a programming error. Not a user error. - assert str_list, "list_plurarlity expect len() >= 1." + assert str_list, "list_plurality expect len() >= 1." # -- Handle the case of a single item. if len(str_list) == 1: @@ -540,7 +435,7 @@ def nameof(*_args) -> List[str]: return list(argname("*_args")) -def debug_decoractor(func): +def debug_decorator(func): """A decorator for dumping the input and output of a function when APIO_DEBUG is defined. Add it to functions and methods that you want to examine with APIO_DEBUG. @@ -554,52 +449,52 @@ def outer(*args): if debug: # -- Print the arguments - secho( + cout( f"\n>>> Function {os.path.basename(func.__code__.co_filename)}" f"/{func.__name__}() BEGIN", - fg="magenta", + style="magenta", ) - secho(" * Arguments:") + cout(" * Arguments:") for arg in args: # -- Print all the key,values if it is a dictionary if isinstance(arg, dict): - secho(" * Dict:") + cout(" * Dict:") for key, value in arg.items(): - secho(f" * {key}: {value}") + cout(f" * {key}: {value}") - # -- Print the plain argument if it is not a dicctionary + # -- Print the plain argument if it is not a dictionary else: - secho(f" * {arg}") - print() + cout(f" * {arg}") + cout() # -- Call the function, dump exceptions, if any. try: result = func(*args) except Exception: if debug: - secho(traceback.format_exc()) + cout(traceback.format_exc()) raise if debug: # -- Print its output - secho(" Returns: ") + cout(" Returns: ") # -- The return object always is a tuple if isinstance(result, tuple): # -- Print all the values in the tuple for value in result: - secho(f" * {value}") + cout(f" * {value}") # -- But just in case it is not a tuple (because of an error...) else: - secho(f" * No tuple: {result}") + cout(f" * No tuple: {result}") - secho( + cout( f"<<< Function {os.path.basename(func.__code__.co_filename)}" f"/{func.__name__}() END\n", - fg="magenta", + style="magenta", ) return result @@ -608,7 +503,7 @@ def outer(*args): def get_apio_version() -> str: - """Returns the version of the apio packge.""" + """Returns the version of the apio package.""" return importlib.metadata.version("apio") @@ -622,18 +517,16 @@ def _check_home_dir(home_dir: Path): home_dir, Path ), f"Error: home_dir is no a Path: {type(home_dir)}, {home_dir}" - # -- The path should be abosolute, see discussion here: + # -- The path should be absolute, see discussion here: # -- https://github.com/FPGAwars/apio/issues/522 if not home_dir.is_absolute(): - secho( - "Error: apio home dir should be an absolute path " - f"[{str(home_dir)}].", - fg="red", + cerror( + "Apio home dir should be an absolute path " f"[{str(home_dir)}].", ) - secho( + cout( "You can use the system env var APIO_HOME_DIR to set " "a different apio home dir.", - fg="yellow", + style="yellow", ) sys.exit(1) @@ -642,26 +535,25 @@ def _check_home_dir(home_dir: Path): # -- See here https://github.com/FPGAwars/apio/issues/515 for ch in str(home_dir): if ord(ch) < 33 or ord(ch) > 127: - secho( - f"Error: Unsupported character [{ch}] in apio home dir: " + cerror( + f"Unsupported character [{ch}] in apio home dir: " f"[{str(home_dir)}].", - fg="red", ) - secho( + cout( "Only the ASCII characters in the range 33 to 127 are " "allowed. You can use the\n" "system env var 'APIO_HOME_DIR' to set a different apio" "home dir.", - fg="yellow", + style="yellow", ) sys.exit(1) def resolve_home_dir() -> Path: """Get the absolute apio home dir. This is the apio folder where the - profle is located and the packages are installed. + profile 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 + variable or in the /etc/apio.json file (in 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 @@ -685,14 +577,14 @@ def resolve_home_dir() -> Path: else: home_dir = Path.home() / ".apio" - # -- Verify that the home dir meets apio's requirments. + # -- Verify that the home dir meets apio's requirements. _check_home_dir(home_dir) # -- Create the folder if it does not exist try: home_dir.mkdir(parents=True, exist_ok=True) except PermissionError: - secho(f"Error: no usable home directory {home_dir}", fg="red") + cerror(f"No usable home directory {home_dir}") sys.exit(1) # Return the home_dir as a Path @@ -701,13 +593,13 @@ def resolve_home_dir() -> Path: def split( s: str, - seperator: str, + separator: str, strip: bool = False, keep_empty: bool = True, ) -> str: """Split a string into parts.""" # -- A workaround for python's "".split(",") returning ['']. - s = s.split(seperator) if s else [] + s = s.split(separator) if s else [] # -- Strip the elements if requested. if strip: @@ -723,15 +615,15 @@ def split( def fpga_arch_sort_key(fpga_arch: str) -> Any: """Given an fpga arch name such as 'ice40', return a sort key - got force our prefered order of sorthing by architecutre. Used in + got force our preferred order of sorting by architecture. Used in reports such as examples, fpgas, and boards.""" - # -- The prefered order of architectures, Add more if adding new + # -- The preferred order of architectures, Add more if adding new # -- architectures. archs = ["ice40", "ecp5", "gowin"] - # -- Primary key with prefered architecuted first and in the - # -- prefered order. + # -- Primary key with preferred architecture first and in the + # -- preferred order. primary_key = archs.index(fpga_arch) if fpga_arch in archs else len(archs) # -- Construct the key, unknown architectures list at the end by diff --git a/pyproject.toml b/pyproject.toml index baeefa4a..158d3879 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,8 @@ requires = [ 'icefunprog==2.0.3', 'apycula==0.15', 'apollo_fpga==1.1.1', - 'protobuf==5.29.3' + 'protobuf==5.29.3', + 'rich==13.9.4' ] [tool.flit.sdist] diff --git a/test/commands/test_apio_build.py b/test/commands/test_apio_build.py index e001a26f..18f199f8 100644 --- a/test/commands/test_apio_build.py +++ b/test/commands/test_apio_build.py @@ -17,7 +17,7 @@ def test_build_without_apio_init(apio_runner: ApioRunner): # -- Run "apio build" without apio.ini result = sb.invoke_apio_cmd(apio, ["build"]) assert result.exit_code != 0, result.output - assert "Error: missing project file apio.ini" in result.output + assert "Error: Missing project file apio.ini" in result.output def test_build_with_apio_init(apio_runner: ApioRunner): @@ -30,13 +30,13 @@ def test_build_with_apio_init(apio_runner: ApioRunner): 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 + assert "Error: Missing option 'board'" 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 + assert "no such board 'no-such-board'" in result.output.lower() # -- Run "apio build" with an unknown option. sb.write_apio_ini( @@ -44,4 +44,4 @@ def test_build_with_apio_init(apio_runner: ApioRunner): ) result = sb.invoke_apio_cmd(apio, ["build"]) assert result.exit_code == 1, result.output - assert "unknown project option 'unknown'" in result.output + assert "Error: Unknown project option 'unknown'" in result.output diff --git a/test/commands/test_apio_clean.py b/test/commands/test_apio_clean.py index 4df48c1a..d0beacfa 100644 --- a/test/commands/test_apio_clean.py +++ b/test/commands/test_apio_clean.py @@ -16,7 +16,7 @@ def test_clean_without_apio_ini(apio_runner: ApioRunner): # -- Run "apio clean" with no apio.ini result = sb.invoke_apio_cmd(apio, ["clean"]) assert result.exit_code != 0, result.output - assert "Error: missing project file apio.ini" in result.output + assert "Error: Missing project file apio.ini" in result.output # R0801: Similar lines in 2 files diff --git a/test/commands/test_apio_create.py b/test/commands/test_apio_create.py index 87d1b81a..f6be95e5 100644 --- a/test/commands/test_apio_create.py +++ b/test/commands/test_apio_create.py @@ -42,7 +42,7 @@ def test_create(apio_runner: ApioRunner): apio, ["create", "--board", "missed_board"] ) assert result.exit_code == 1, result.output - assert "Error: no such board" in result.output + assert "Error: No such board" in result.output assert not exists(apio_ini) # -- Execute "apio create --board alhambra-ii" @@ -60,7 +60,7 @@ def test_create(apio_runner: ApioRunner): ["create", "--board", "alhambra-ii", "--top-module", "my_module"], ) assert result.exit_code != 0 - assert "the file apio.ini already exists" in result.output + assert "Error: The file apio.ini already exists." in result.output _check_ini_file( apio_ini, {"board": "alhambra-ii", "top-module": "main"} ) diff --git a/test/commands/test_apio_docs.py b/test/commands/test_apio_docs.py new file mode 100644 index 00000000..0a4c64b5 --- /dev/null +++ b/test/commands/test_apio_docs.py @@ -0,0 +1,48 @@ +""" + Test for the "apio docs" command +""" + +from test.conftest import ApioRunner +from apio.common.apio_console import cunstyle +from apio.commands.apio import cli as apio + + +def test_apio_docs(apio_runner: ApioRunner): + """Tests the apio docs commands.""" + + with apio_runner.in_sandbox() as sb: + # -- Execute "apio docs" + result = sb.invoke_apio_cmd(apio, ["docs"]) + sb.assert_ok(result) + assert "apio docs options" in cunstyle(result.output) + assert result.output != cunstyle(result.output) # Colored. + + # -- Execute "apio docs" (pipe mode) + result = sb.invoke_apio_cmd(apio, ["docs"], terminal_mode=False) + sb.assert_ok(result) + assert "apio docs options" in cunstyle(result.output) + assert result.output == cunstyle(result.output) # Colored. + + # -- Execute "apio docs options" + result = sb.invoke_apio_cmd(apio, ["docs", "options"]) + assert result.exit_code == 0 + assert "BOARD (REQUIRED)" in cunstyle(result.output) + assert "YOSYS-SYNTH-EXTRA-OPTIONS (OPTIONAL)" in cunstyle( + result.output + ) + assert result.output != cunstyle(result.output) # Colored. + + # -- Execute "apio docs options board" + result = sb.invoke_apio_cmd(apio, ["docs", "options", "board"]) + assert result.exit_code == 0 + assert "BOARD (REQUIRED)" in cunstyle(result.output) + assert "YOSYS-SYNTH-EXTRA-OPTIONS (OPTIONAL)" not in cunstyle( + result.output + ) + assert result.output != cunstyle(result.output) # Colored. + + # # -- Execute "apio docs resources" + result = sb.invoke_apio_cmd(apio, ["docs", "resources"]) + assert result.exit_code == 0 + assert "Apio documentation" in result.output + assert result.output != cunstyle(result.output) # Colored. diff --git a/test/commands/test_apio_drivers.py b/test/commands/test_apio_drivers.py index 45033294..5d57c27a 100644 --- a/test/commands/test_apio_drivers.py +++ b/test/commands/test_apio_drivers.py @@ -3,7 +3,7 @@ """ from test.conftest import ApioRunner -from click.termui import unstyle +from apio.common.apio_console import cunstyle from apio.commands.apio import cli as apio @@ -15,6 +15,6 @@ def test_drivers(apio_runner: ApioRunner): # -- Execute "apio drivers" result = sb.invoke_apio_cmd(apio, "drivers") sb.assert_ok(result) - assert "apio drivers list" in unstyle(result.output) - assert "apio drivers install" in unstyle(result.output) - assert "apio drivers uninstall" in unstyle(result.output) + assert "apio drivers list" in cunstyle(result.output) + assert "apio drivers install" in cunstyle(result.output) + assert "apio drivers uninstall" in cunstyle(result.output) diff --git a/test/commands/test_apio_examples.py b/test/commands/test_apio_examples.py index a5d8a5cc..78d89141 100644 --- a/test/commands/test_apio_examples.py +++ b/test/commands/test_apio_examples.py @@ -3,7 +3,7 @@ """ from test.conftest import ApioRunner -from click.termui import unstyle +from apio.common.apio_console import cunstyle from apio.commands.apio import cli as apio @@ -15,5 +15,5 @@ def test_examples(apio_runner: ApioRunner): # -- Execute "apio examples" result = sb.invoke_apio_cmd(apio, ["examples"]) sb.assert_ok(result) - assert "Subcommands:" in unstyle(result.output) - assert "examples list" in unstyle(result.output) + assert "Subcommands:" in cunstyle(result.output) + assert "examples list" in cunstyle(result.output) diff --git a/test/commands/test_apio_format.py b/test/commands/test_apio_format.py index 85194c35..93233019 100644 --- a/test/commands/test_apio_format.py +++ b/test/commands/test_apio_format.py @@ -14,4 +14,4 @@ def test_format_without_apio_ini(apio_runner: ApioRunner): # -- Run "apio format" with no apio.ini result = sb.invoke_apio_cmd(apio, ["format"]) assert result.exit_code != 0, result.output - assert "Error: missing project file apio.ini" in result.output + assert "Error: Missing project file apio.ini" in result.output diff --git a/test/commands/test_apio_graph.py b/test/commands/test_apio_graph.py index 3329d655..c0ed7380 100644 --- a/test/commands/test_apio_graph.py +++ b/test/commands/test_apio_graph.py @@ -16,4 +16,4 @@ def test_graph_no_apio_ini(apio_runner: ApioRunner): # -- Execute "apio graph" result = sb.invoke_apio_cmd(apio, ["graph"]) assert result.exit_code == 1, result.output - assert "Error: missing project file apio.ini" in result.output + assert "Error: Missing project file apio.ini" in result.output diff --git a/test/commands/test_apio_lint.py b/test/commands/test_apio_lint.py index 5c7dfea0..05d0ab8f 100644 --- a/test/commands/test_apio_lint.py +++ b/test/commands/test_apio_lint.py @@ -14,4 +14,4 @@ def test_lint_apio_init(apio_runner: ApioRunner): # -- Execute "apio lint" result = sb.invoke_apio_cmd(apio, ["lint"]) assert result.exit_code == 1, result.output - assert "missing project file apio.ini" in result.output + assert "Error: Missing project file apio.ini" in result.output diff --git a/test/commands/test_apio_packages.py b/test/commands/test_apio_packages.py index 8d53d0e0..ec20f791 100644 --- a/test/commands/test_apio_packages.py +++ b/test/commands/test_apio_packages.py @@ -4,6 +4,7 @@ from test.conftest import ApioRunner from apio.commands.apio import cli as apio +from apio.common.apio_console import cunstyle def test_packages(apio_runner: ApioRunner): @@ -15,7 +16,8 @@ def test_packages(apio_runner: ApioRunner): result = sb.invoke_apio_cmd(apio, ["packages"]) sb.assert_ok(result) assert "Subcommands:" in result.output - assert "apio packages install" in result.output + assert "apio packages install" in cunstyle(result.output) + assert result.output != cunstyle(result.output) # Colored. # -- Execute "apio packages list" result = sb.invoke_apio_cmd(apio, ["packages", "list"]) @@ -26,14 +28,14 @@ def test_packages(apio_runner: ApioRunner): apio, ["packages", "install", "no-such-package"] ) assert result.exit_code == 1, result.output - assert "Error: no such package 'no-such-package'" in result.output + assert "Error: No such package 'no-such-package'" in result.output # -- Execute "apio packages uninstall no-such-package" result = sb.invoke_apio_cmd( apio, ["packages", "uninstall", "no-such-package"] ) assert result.exit_code == 1, result.output - assert "Error: no such package 'no-such-package'" in result.output + assert "Error: No such package 'no-such-package'" in result.output # -- Execute "apio packages fix" result = sb.invoke_apio_cmd(apio, ["packages", "fix"]) diff --git a/test/commands/test_apio_prereferences.py b/test/commands/test_apio_preferences.py similarity index 58% rename from test/commands/test_apio_prereferences.py rename to test/commands/test_apio_preferences.py index fa1fa8d8..698038ff 100644 --- a/test/commands/test_apio_prereferences.py +++ b/test/commands/test_apio_preferences.py @@ -2,8 +2,9 @@ Test for the "apio preferences" command """ +import re from test.conftest import ApioRunner -from click import unstyle +from apio.common.apio_console import cunstyle from apio.commands.apio import cli as apio @@ -14,37 +15,38 @@ def test_colors_on_off(apio_runner: ApioRunner): # -- Execute "apio preferences set --colors on" result = sb.invoke_apio_cmd( - apio, ["preferences", "set", "--colors", "on"], color=True + apio, ["preferences", "set", "--colors", "on"] ) sb.assert_ok(result) - assert "\n\x1b[32m\x1b[1mColors set to [on]\x1b[0m\n" in result.output + assert "Colors set to [on]" in result.output + assert result.output != cunstyle(result.output) # Colored. # -- Execute "apio preferences list". It should reports colors on, # -- in colors. - result = sb.invoke_apio_cmd(apio, ["preferences", "list"], color=True) + result = sb.invoke_apio_cmd(apio, ["preferences", "list"]) sb.assert_ok(result) - assert "\nColors: \x1b[36m\x1b[1mon\x1b[0m\n" in result.output + assert result.output != cunstyle(result.output) # Colored. # -- Execute "apio system info". It should emit colors. - result = sb.invoke_apio_cmd(apio, ["system", "info"], color=True) + result = sb.invoke_apio_cmd(apio, ["system", "info"]) sb.assert_ok(result) - assert result.output != unstyle(result.output) + assert result.output != cunstyle(result.output) # Colored. # -- Execute "apio preferences set --colors off" result = sb.invoke_apio_cmd( - apio, ["preferences", "set", "--colors", "off"], color=True + apio, ["preferences", "set", "--colors", "off"] ) sb.assert_ok(result) - assert "\nColors set to [off]\n" in result.output - print(result.output) + assert "Colors set to [off]" in result.output # -- Execute "apio preferences list". It should reports colors off, # -- without colors. - result = sb.invoke_apio_cmd(apio, ["preferences", "list"], color=True) + result = sb.invoke_apio_cmd(apio, ["preferences", "list"]) sb.assert_ok(result) - assert "\nColors: off\n" in result.output + assert re.search(r"Colors.*off", result.output), result.output + assert result.output == cunstyle(result.output) # Non colored.. # -- Execute "apio system info". It should not emit colors. - result = sb.invoke_apio_cmd(apio, ["system", "info"], color=True) + result = sb.invoke_apio_cmd(apio, ["system", "info"]) sb.assert_ok(result) - assert result.output == unstyle(result.output) + assert result.output == cunstyle(result.output) diff --git a/test/commands/test_apio_raw.py b/test/commands/test_apio_raw.py index cf6c993d..ec0a47fd 100644 --- a/test/commands/test_apio_raw.py +++ b/test/commands/test_apio_raw.py @@ -22,6 +22,6 @@ def test_raw(apio_runner: ApioRunner): # -- Execute "apio raw -v" result = sb.invoke_apio_cmd(apio, ["raw", "-v"]) assert result.exit_code == 0, result.output - assert "Envirnment settings:" in result.output + assert "Environment settings:" in result.output assert "PATH" in result.output assert "YOSYS_LIB" in result.output diff --git a/test/commands/test_apio_report.py b/test/commands/test_apio_report.py index ce22ed9c..efb65f57 100644 --- a/test/commands/test_apio_report.py +++ b/test/commands/test_apio_report.py @@ -16,4 +16,4 @@ def test_report_no_apio(apio_runner: ApioRunner): # -- Run "apio report" without apio.ini result = sb.invoke_apio_cmd(apio, ["report"]) assert result.exit_code != 0, result.output - assert "Error: missing project file apio.ini" in result.output + assert "Error: Missing project file apio.ini" in result.output diff --git a/test/commands/test_apio_test.py b/test/commands/test_apio_test.py index 011330de..f655ae95 100644 --- a/test/commands/test_apio_test.py +++ b/test/commands/test_apio_test.py @@ -17,4 +17,4 @@ def test_test(apio_runner: ApioRunner): # -- Execute "apio test" result = sb.invoke_apio_cmd(apio, ["test"]) assert result.exit_code != 0, result.output - assert "Error: missing project file apio.ini" in result.output + assert "Error: Missing project file apio.ini" in result.output diff --git a/test/commands/test_apio_upload.py b/test/commands/test_apio_upload.py index 1accc2a4..172a4014 100644 --- a/test/commands/test_apio_upload.py +++ b/test/commands/test_apio_upload.py @@ -21,4 +21,4 @@ def test_upload_without_apio_ini(apio_runner: ApioRunner): # -- Check the result assert result.exit_code == 1, result.output - assert "Error: missing project file apio.ini" in result.output + assert "Error: Missing project file apio.ini" in result.output diff --git a/test/common/test_apio_console.py b/test/common/test_apio_console.py new file mode 100644 index 00000000..50817518 --- /dev/null +++ b/test/common/test_apio_console.py @@ -0,0 +1,23 @@ +"""Test for the apio_console.py.""" + +from apio.common.apio_console import cstyle, cunstyle + + +def test_style_unstyle(): + """Test the styling and unstyling functions""" + + # -- Test cstyle() + assert cstyle("") == "" + assert cstyle("", style="red") == "" + assert cstyle("abc xyz", style="red") == "\x1b[31mabc xyz\x1b[0m" + assert cstyle("abc xyz", style="cyan bold") == "\x1b[1;36mabc xyz\x1b[0m" + assert cstyle("ab \n xy", style="cyan bold") == "\x1b[1;36mab \n xy\x1b[0m" + + # -- Test cunstyle() with plain text. + assert cunstyle("") == "" + assert cunstyle("abc xyz") == "abc xyz" + + # -- Test cunstyle() with colored text. + assert cunstyle(cstyle("")) == "" + assert cunstyle(cstyle("abc xyz")) == "abc xyz" + assert cunstyle(cstyle("ab \n xy")) == "ab \n xy" diff --git a/test/conftest.py b/test/conftest.py index 648b4d72..cd4ef5f1 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -10,16 +10,10 @@ from typing import List, Union, cast, Optional from typing import Dict import os - import pytest +from click.testing import CliRunner, Result +from apio.common import apio_console -# -- Class for executing click commands -# https://click.palletsprojects.com/en/8.1.x/api/#click.testing.CliRunner -from click.testing import CliRunner - -# -- Class for storing the results of executing a click command -# https://click.palletsprojects.com/en/8.1.x/api/#click.testing.Result -from click.testing import Result # -- Debug mode on/off DEBUG = True @@ -28,7 +22,7 @@ SANDBOX_MARKER = "apio-sandbox" -# -- This function is called by pytest. It addes the pytest --offline flag +# -- This function is called by pytest. It adds the pytest --offline flag # -- which is is passed to tests that ask for it using the fixture # -- 'offline_flag' below. # -- @@ -37,8 +31,8 @@ def pytest_addoption(parser: pytest.Parser): """Register the --offline command line option when invoking pytest""" # -- Option: --offline - # -- It is used by the function test that requieres - # -- internet connnection for testing + # -- It is used by the function test that requires + # -- internet connection for testing parser.addoption( "--offline", action="store_true", help="Run tests in offline mode" ) @@ -71,19 +65,19 @@ def expired(self) -> bool: @property def sandbox_dir(self) -> Path: """Returns the sandbox's dir.""" - assert not self.expired, "Sanbox expired" + assert not self.expired, "Sandbox expired" return self._sandbox_dir @property def proj_dir(self) -> Path: """Returns the sandbox's apio project dir.""" - assert not self.expired, "Sanbox expired" + assert not self.expired, "Sandbox expired" return self._proj_dir @property def home_dir(self) -> Path: """Returns the sandbox's apio home dir.""" - assert not self.expired, "Sanbox expired" + assert not self.expired, "Sandbox expired" return self._home_dir @property @@ -101,10 +95,11 @@ def invoke_apio_cmd( self, cli, args=None, + *, input=None, env=None, catch_exceptions=True, - color=False, + terminal_mode=True, **extra, ): """Invoke an apio command.""" @@ -125,6 +120,10 @@ def invoke_apio_cmd( # -- check that the test didn't corrupt them. assert os.environ["APIO_HOME_DIR"] == str(self.home_dir) + # -- If True, force terminal mode, if False, forces pipe mode, + # -- otherwise auto which is pipe mode under pytest. + apio_console.configure(force_terminal=terminal_mode) + # -- Invoke the command. Get back the collected results. result = self._click_runner.invoke( cli=cli, @@ -132,7 +131,7 @@ def invoke_apio_cmd( input=input, env=env, catch_exceptions=catch_exceptions, - color=color, + color=terminal_mode, **extra, ) @@ -155,15 +154,15 @@ def assert_ok(self, result: Result): assert "error" not in result.output.lower() def set_system_env(self, new_vars: Dict[str, str]) -> None: - """Overwirte the existing sys.environ with the given dirct. Vars + """Overwrites the existing sys.environ with the given dict. Vars that are not in the dict are deleted and vars that have a different value in the dict is updated. Can be called only within a an apio sandbox.""" - # -- Check that the sandox not expired. - assert not self.expired, "Sandox expired" + # -- Check that the sandbox not expired. + assert not self.expired, "Sandbox expired" - # -- NOTE: naivly assining the dict to os.environ will break + # -- NOTE: naively assigning the dict to os.environ will break # -- os.environ since a simple dict doesn't update the underlying # -- system env when it's mutated. @@ -229,7 +228,7 @@ def read_file( def write_apio_ini(self, properties: Dict[str, str]): """Write in the current directory an apio.ini file with given - values. If an apio.ini file alread exists, it is overwritten.""" + values. If an apio.ini file already exists, it is overwritten.""" assert isinstance(properties, dict), "Not a dict." @@ -310,12 +309,12 @@ def in_sandbox(self, shared_home: bool = False): sandbox uses a unique apio shared home directory or shares it with other sandboxes in the same apio_runner scope that set it to True. - Upoon return, the current directory is proj_dir. + Upon return, the current directory is proj_dir. """ # -- Make sure we don't try to nest sandboxes. assert self._sandbox is None, "Already in a sandbox." - # -- Snatpshot the system env. + # -- Snapshot the system env. original_env: Dict[str, str] = os.environ.copy() # -- Snapshot the current directory. @@ -326,7 +325,7 @@ def in_sandbox(self, shared_home: bool = False): sandbox_dir = Path(tempfile.mkdtemp(prefix=SANDBOX_MARKER + "-")) # -- Make the sandbox's project directory. We intentionally use a - # -- directory name with a space and a non ascii characterto test + # -- directory name with a space and a non ascii character to test # -- that apio can handle it. proj_dir = sandbox_dir / "apio prój" proj_dir.mkdir() @@ -356,7 +355,7 @@ def in_sandbox(self, shared_home: bool = False): print(f" apio home dir : {str(home_dir)}") print() - # -- Register a sanbox objet to indicate that we are in a sandbox. + # -- Register a sandbox objet to indicate that we are in a sandbox. assert self._sandbox is None self._sandbox = ApioSandbox(self, sandbox_dir, proj_dir, home_dir) @@ -364,10 +363,14 @@ def in_sandbox(self, shared_home: bool = False): # -- home and packages dirs. os.environ["APIO_HOME_DIR"] = str(home_dir) + # -- Reset the apio console, since we run multiple sandboxes in the + # -- same process. + apio_console.reset() + try: # -- This is the end of the context manager _entry part. The - # -- call to _exit will continue execution after the yeield. - # -- Value is the sandox object we pass to the user. + # -- call to _exit will continue execution after the yield. + # -- Value is the sandbox object we pass to the user. yield cast(ApioSandbox, self._sandbox) finally: @@ -377,7 +380,7 @@ def in_sandbox(self, shared_home: bool = False): # -- Restore the original system env. self._sandbox.set_system_env(original_env) - # -- Mark that we exited the sanbox. This expires the sandbox. + # -- Mark that we exited the sandbox. This expires the sandbox. self._sandbox = None # -- Change back to the original directory. diff --git a/test/integration/test_commands.py b/test/integration/test_commands.py index 57fabc65..603e1f1b 100644 --- a/test/integration/test_commands.py +++ b/test/integration/test_commands.py @@ -70,7 +70,7 @@ def test_boards_list_ok(apio_runner: ApioRunner): result = sb.invoke_apio_cmd(apio, ["boards"]) sb.assert_ok(result) assert "Loading custom 'boards.jsonc'" not in result.output - assert "PACK" not in result.output + assert "FPGA-ID" not in result.output assert "alhambra-ii" in result.output assert "my_custom_board" not in result.output assert "Total of 1 board" not in result.output @@ -79,7 +79,7 @@ def test_boards_list_ok(apio_runner: ApioRunner): result = sb.invoke_apio_cmd(apio, ["boards", "-v"]) sb.assert_ok(result) assert "Loading custom 'boards.jsonc'" not in result.output - assert "PACK" in result.output + assert "FPGA-ID" in result.output assert "alhambra-ii" in result.output assert "my_custom_board" not in result.output assert "Total of 1 board" not in result.output @@ -100,7 +100,7 @@ def test_utilities(apio_runner: ApioRunner): # -- Run 'apio upgrade' result = sb.invoke_apio_cmd(apio, ["upgrade"]) sb.assert_ok(result) - assert "Lastest Apio stable version" in result.output + assert "Latest Apio stable version" in result.output # -- Run 'apio raw "nextpnr-ice40 --help"' result = sb.invoke_apio_cmd( @@ -111,7 +111,7 @@ def test_utilities(apio_runner: ApioRunner): # -- Run 'apio raw -v' result = sb.invoke_apio_cmd(apio, ["raw", "-v"]) sb.assert_ok(result) - assert "Envirnment settings:" in result.output + assert "Environment settings:" in result.output assert "YOSYS_LIB" in result.output @@ -125,7 +125,7 @@ def test_project_with_legacy_board_name(apio_runner: ApioRunner): # -- We shared the apio home with the other tests in this file to speed # -- up apio package installation. Tests should not mutate the shared home - # -- to avoid cross-interferance between tests in this file. + # -- to avoid cross-interference between tests in this file. with apio_runner.in_sandbox(shared_home=True) as sb: # -- Fetch an example of a board that has a legacy name. @@ -146,7 +146,7 @@ def test_project_with_legacy_board_name(apio_runner: ApioRunner): result = sb.invoke_apio_cmd(apio, ["clean"]) sb.assert_ok(result) - # -- Run 'apio build' again. It should also succeeed. + # -- Run 'apio build' again. It should also succeed. result = sb.invoke_apio_cmd(apio, ["build"]) sb.assert_ok(result) @@ -162,7 +162,7 @@ def _test_project( *, remote_proj_dir: bool, example: str, - testbench: str, + testbench_file: str, bitstream: str, report_item: str, ): @@ -175,9 +175,12 @@ def _test_project( if apio_runner.offline_flag: pytest.skip("requires internet connection") + # -- Extract the base name of the testbench file + testbench, _ = os.path.splitext(testbench_file) + # -- We shared the apio home with the other tests in this file to speed # -- up apio package installation. Tests should not mutate the shared home - # -- to avoid cross-interferance between tests in this file. + # -- to avoid cross-interference between tests in this file. with apio_runner.in_sandbox(shared_home=True) as sb: # -- If testing from a remote dir, step out of the proj dir, and @@ -262,13 +265,13 @@ def _test_project( assert not (sb.proj_dir / f"_build/{testbench}.vcd").exists() # -- 'apio test ' - result = sb.invoke_apio_cmd( - apio, ["test", testbench + ".v"] + proj_arg - ) + result = sb.invoke_apio_cmd(apio, ["test", testbench_file] + proj_arg) sb.assert_ok(result) assert "SUCCESS" in result.output assert getsize(sb.proj_dir / f"_build/{testbench}.out") assert getsize(sb.proj_dir / f"_build/{testbench}.vcd") + # -- For issue https://github.com/FPGAwars/apio/issues/557 + assert "warning: Timing checks are not supported" not in result.output # -- 'apio report' result = sb.invoke_apio_cmd(apio, ["report"] + proj_arg) @@ -297,14 +300,13 @@ def _test_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/bcd-counter", - testbench="main_tb", + testbench_file="main_tb.v", bitstream="hardware.bin", - report_item="ICESTORM_LC:", + report_item="ICESTORM_LC", ) @@ -315,9 +317,22 @@ def test_project_ice40_remote_dir(apio_runner: ApioRunner): apio_runner, remote_proj_dir=True, example="alhambra-ii/bcd-counter", - testbench="main_tb", + testbench_file="main_tb.v", bitstream="hardware.bin", - report_item="ICESTORM_LC:", + report_item="ICESTORM_LC", + ) + + +def test_project_ice40_system_verilog(apio_runner: ApioRunner): + """Tests building and testing an ice40 project that contains system + verilog files.""" + _test_project( + apio_runner, + remote_proj_dir=False, + example="alhambra-ii/bcd-counter-sv", + testbench_file="main_tb.sv", + bitstream="hardware.bin", + report_item="ICESTORM_LC", ) @@ -327,9 +342,9 @@ def test_project_ecp5_local_dir(apio_runner: ApioRunner): apio_runner, remote_proj_dir=False, example="colorlight-5a-75b-v8/ledon", - testbench="ledon_tb", + testbench_file="ledon_tb.v", bitstream="hardware.bit", - report_item="ALU54B:", + report_item="ALU54B", ) @@ -339,9 +354,22 @@ def test_project_ecp5_remote_dir(apio_runner: ApioRunner): apio_runner, remote_proj_dir=True, example="colorlight-5a-75b-v8/ledon", - testbench="ledon_tb", + testbench_file="ledon_tb.v", bitstream="hardware.bit", - report_item="ALU54B:", + report_item="ALU54B", + ) + + +def test_project_ecp5_system_verilog(apio_runner: ApioRunner): + """Tests building and testing an ecp5 project that contains system + verilog files.""" + _test_project( + apio_runner, + remote_proj_dir=False, + example="colorlight-5a-75b-v8/ledon-sv", + testbench_file="ledon_tb.sv", + bitstream="hardware.bit", + report_item="ALU54B", ) @@ -350,10 +378,10 @@ def test_project_gowin_local_dir(apio_runner: ApioRunner): _test_project( apio_runner, remote_proj_dir=False, - example="sipeed-tang-nano-4k/blinky", - testbench="blinky_tb", + example="sipeed-tang-nano-9k/blinky", + testbench_file="blinky_tb.v", bitstream="hardware.fs", - report_item="ALU54D:", + report_item="LUT4", ) @@ -362,8 +390,21 @@ def test_project_gowin_remote_dir(apio_runner: ApioRunner): _test_project( apio_runner, remote_proj_dir=True, - example="sipeed-tang-nano-4k/blinky", - testbench="blinky_tb", + example="sipeed-tang-nano-9k/blinky", + testbench_file="blinky_tb.v", + bitstream="hardware.fs", + report_item="LUT4", + ) + + +def test_project_gowin_system_verilog(apio_runner: ApioRunner): + """Tests building and testing an gowin project that contains system + verilog files.""" + _test_project( + apio_runner, + remote_proj_dir=False, + example="sipeed-tang-nano-9k/blinky-sv", + testbench_file="blinky_tb.sv", bitstream="hardware.fs", - report_item="ALU54D:", + report_item="LUT4", ) diff --git a/test/integration/test_examples.py b/test/integration/test_examples.py index c528abf0..df1c0512 100644 --- a/test/integration/test_examples.py +++ b/test/integration/test_examples.py @@ -27,10 +27,11 @@ def test_examples(apio_runner: ApioRunner): assert "alhambra-ii/ledon" in result.output assert "Turning on a led" in result.output - # -- 'apio examples fetch alhambra-ii/ledon' + # -- 'apio examples fetch alhambra-ii/ledon' (colors off) result = sb.invoke_apio_cmd( apio, ["examples", "fetch", "alhambra-ii/ledon"], + terminal_mode=False, ) sb.assert_ok(result) assert "Copying alhambra-ii/ledon example files" in result.output @@ -39,30 +40,33 @@ def test_examples(apio_runner: ApioRunner): assert "Example fetched successfully" in result.output assert getsize("ledon.v") - # -- 'apio examples fetch-board alhambra-ii' + # -- 'apio examples fetch-board alhambra-ii' (colors off) result = sb.invoke_apio_cmd( apio, ["examples", "fetch-board", "alhambra-ii"], + terminal_mode=False, ) sb.assert_ok(result) assert "Creating directory alhambra-ii" in result.output assert "Board examples fetched successfully" in result.output assert getsize("alhambra-ii/ledon/ledon.v") - # -- 'apio examples fetch alhambra-ii/ledon -d dir1' + # -- 'apio examples fetch alhambra-ii/ledon -d dir1' (colors off) result = sb.invoke_apio_cmd( apio, ["examples", "fetch", "alhambra-ii/ledon", "-d", "dir1"], + terminal_mode=False, ) sb.assert_ok(result) assert "Copying alhambra-ii/ledon example files" in result.output assert "Example fetched successfully" in result.output assert getsize("dir1/ledon.v") - # -- 'apio examples fetch-board alhambra -d dir2 + # -- 'apio examples fetch-board alhambra -d dir2 (colors off) result = sb.invoke_apio_cmd( apio, ["examples", "fetch-board", "alhambra-ii", "-d", "dir2"], + terminal_mode=False, ) sb.assert_ok(result) assert f"Creating directory dir2{os.sep}alhambra-ii" in result.output diff --git a/test/integration/test_packages.py b/test/integration/test_packages.py index 57cb99a2..40adb869 100644 --- a/test/integration/test_packages.py +++ b/test/integration/test_packages.py @@ -62,7 +62,6 @@ def test_packages(apio_runner: ApioRunner): # -- This should not do anything since it's considered to be installed. result = sb.invoke_apio_cmd(apio, ["packages", "install", "examples"]) sb.assert_ok(result) - assert "was already install" in result.output assert "Package 'examples' installed" not in result.output assert not marker_file.exists() @@ -75,7 +74,7 @@ def test_packages(apio_runner: ApioRunner): assert "Package 'examples' installed" in result.output assert marker_file.is_file() - # -- Uninstall the examples package. It should delete the exemples + # -- Uninstall the examples package. It should delete the examples # -- package and will leave the rest. assert "examples" in listdir(sb.packages_dir) result = sb.invoke_apio_cmd( diff --git a/test/managers/test_project.py b/test/managers/test_project.py index d5a8f3df..1cd451c2 100644 --- a/test/managers/test_project.py +++ b/test/managers/test_project.py @@ -17,7 +17,7 @@ def test_required_and_optionals(apio_runner: ApioRunner): # -- Create an apio.ini. sb.write_apio_ini( { - # -- Requied. + # -- Required. "board": "alhambra-ii", # -- Optional. "top-module": "my_module", diff --git a/test/managers/test_scons.py b/test/managers/test_scons.py index e67be1ff..01b1bc9b 100644 --- a/test/managers/test_scons.py +++ b/test/managers/test_scons.py @@ -4,7 +4,7 @@ from test.conftest import ApioRunner from google.protobuf import text_format -from apio.proto.apio_pb2 import ( +from apio.common.proto.apio_pb2 import ( SconsParams, Verbosity, TargetParams, @@ -17,7 +17,7 @@ # pylint: disable=R0801 TEST_APIO_INI_DICT = { - # -- Requied. + # -- Required. "board": "alhambra-ii", # -- Optional. "top-module": "my_module", @@ -39,8 +39,9 @@ pack: "tq144:4k" } } -envrionment { - platform_id: "darwin_arm64" +environment { + platform_id: "TBD" + is_windows: true # TBD is_debug: false yosys_path: "TBD" trellis_path: "TBD" @@ -50,6 +51,7 @@ top_module: "my_module" yosys_synth_extra_options: "-dsp -xyz" } +# rich_lib_windows_params is TBD """ EXPECTED2 = """ @@ -69,8 +71,9 @@ synth: true pnr: true } -envrionment { - platform_id: "darwin_arm64" +environment { + platform_id: "TBD" + is_windows: true # TBD is_debug: false yosys_path: "TBD" trellis_path: "TBD" @@ -91,6 +94,7 @@ verilator_warns: "dd" } } +# rich_lib_windows_params is TBD """ @@ -110,12 +114,26 @@ def test_default_params(apio_runner: ApioRunner): # -- Construct the expected value. We fill in non deterministic values. expected = text_format.Parse(EXPECTED1, SconsParams()) expected.timestamp = scons_params.timestamp - expected.envrionment.yosys_path = str( + expected.environment.yosys_path = str( sb.packages_dir / "oss-cad-suite/share/yosys" ) - expected.envrionment.trellis_path = str( + expected.environment.trellis_path = str( sb.packages_dir / "oss-cad-suite/share/trellis" ) + expected.environment.platform_id = apio_ctx.platform_id + expected.environment.is_windows = apio_ctx.is_windows + + # The field rich_lib_windows_params is too dynamic so we just assert + # for its existence and remove it from the comparison. + if apio_ctx.is_windows: + assert scons_params.HasField( + "rich_lib_windows_params" + ), scons_params + else: + assert not scons_params.HasField( + "rich_lib_windows_params" + ), scons_params + scons_params.ClearField("rich_lib_windows_params") # -- Compare actual to expected values. assert str(scons_params) == str(expected) @@ -149,12 +167,26 @@ def test_explicit_params(apio_runner: ApioRunner): # -- Construct the expected value. We fill in non deterministic values. expected = text_format.Parse(EXPECTED2, SconsParams()) expected.timestamp = scons_params.timestamp - expected.envrionment.yosys_path = str( + expected.environment.yosys_path = str( sb.packages_dir / "oss-cad-suite/share/yosys" ) - expected.envrionment.trellis_path = str( + expected.environment.trellis_path = str( sb.packages_dir / "oss-cad-suite/share/trellis" ) + expected.environment.platform_id = apio_ctx.platform_id + expected.environment.is_windows = apio_ctx.is_windows + + # The field rich_lib_windows_params is too dynamic so we just assert + # for its existence and remove it from the comparison. + if apio_ctx.is_windows: + assert scons_params.HasField( + "rich_lib_windows_params" + ), scons_params + else: + assert not scons_params.HasField( + "rich_lib_windows_params" + ), scons_params + scons_params.ClearField("rich_lib_windows_params") # -- Compare actual to expected values. assert str(scons_params) == str(expected) diff --git a/test/managers/test_scons_filters.py b/test/managers/test_scons_filters.py index 194d1e77..c0da8093 100644 --- a/test/managers/test_scons_filters.py +++ b/test/managers/test_scons_filters.py @@ -9,14 +9,14 @@ def test_pnr_range_detector(): - """Tests the pnr reange class.""" + """Tests the pnr range class.""" # -- Create a PNR range detector. rd = PnrRangeDetector() # -- Starting out of range - assert not rd.update(PipeId.STDOUT, "hellow world") - assert not rd.update(PipeId.STDOUT, "hellow world") + assert not rd.update(PipeId.STDOUT, "hello world") + assert not rd.update(PipeId.STDOUT, "hello world") # -- Start of range trigger (from next line) assert not rd.update(PipeId.STDOUT, "nextpnr-ice40 bla bla") diff --git a/test/scons/test_apio_env.py b/test/scons/test_apio_env.py index 6e269e77..c6572a35 100644 --- a/test/scons/test_apio_env.py +++ b/test/scons/test_apio_env.py @@ -16,14 +16,14 @@ def test_env_is_debug(): def test_env_platform_id(): - """Tests the env handling of the paltform_id param.""" + """Tests the env handling of the platform_id param.""" # -- Test with a non windows platform id. - env = make_test_apio_env(platform_id="darwin_arm64") + env = make_test_apio_env(platform_id="darwin_arm64", is_windows=False) assert not env.is_windows # -- Test with a windows platform id. - env = make_test_apio_env(platform_id="windows_amd64") + env = make_test_apio_env(platform_id="windows_amd64", is_windows=True) assert env.is_windows diff --git a/test/scons/test_plugin_util.py b/test/scons/test_plugin_util.py index d1933784..8a66a2a5 100644 --- a/test/scons/test_plugin_util.py +++ b/test/scons/test_plugin_util.py @@ -10,7 +10,8 @@ from SCons.Node.FS import FS from SCons.Script import SetOption from pytest import LogCaptureFixture -from apio.proto.apio_pb2 import TargetParams, UploadParams +from apio.common.apio_console import cunstyle +from apio.common.proto.apio_pb2 import TargetParams, UploadParams from apio.scons.plugin_util import ( get_constraint_file, verilog_src_scanner, @@ -38,7 +39,7 @@ def test_get_constraint_file( capsys.readouterr() # Reset capture result = get_constraint_file(apio_env, ".pcf", "my_main") captured = capsys.readouterr() - assert "assuming 'my_main.pcf'" in captured.out + assert "assuming 'my_main.pcf'" in cunstyle(captured.out) assert result == "my_main.pcf" # -- If a single .pcf file, return it. @@ -48,14 +49,14 @@ def test_get_constraint_file( assert captured.out == "" assert result == "pinout.pcf" - # -- If thre is more than one, exit with an error message. + # -- If there is more than one, exit with an error message. sb.write_file("other.pcf", "content") capsys.readouterr() # Reset capture with pytest.raises(SystemExit) as e: result = get_constraint_file(apio_env, ".pcf", "my_main") captured = capsys.readouterr() assert e.value.code == 1 - assert "Error: Found multiple '*.pcf'" in captured.out + assert "Error: Found multiple '*.pcf'" in cunstyle(captured.out) def test_verilog_src_scanner(apio_runner: ApioRunner): @@ -125,7 +126,7 @@ def test_verilog_src_scanner(apio_runner: ApioRunner): # -- Run the scanner again dependency_files = scanner.function(file, apio_env, None) - # -- Check the dependnecies + # -- Check the dependencies file_names = [f.name for f in dependency_files] assert file_names == sorted(core_dependencies + file_dependencies) @@ -260,7 +261,7 @@ def test_make_verilator_config_builder(apio_runner: ApioRunner): builder.action(target, [], apio_env.scons_env) assert isfile("hardware.vlt") - # -- Verify that the file was created with the tiven text. + # -- Verify that the file was created with the given text. text = sb.read_file("hardware.vlt") assert "verilator_config" in text, text assert "lint_off -rule COMBDLY" in text, text @@ -300,9 +301,9 @@ def test_clean_if_requested(apio_runner: ApioRunner): items_list = list(SconsHacks.get_targets().items()) target, dependencies = items_list[0] - # -- Verify the tartget name, hard coded in set_up_cleanup() + # -- Verify the target name, hard coded in set_up_cleanup() assert target.name == "cleanup-target" - # -- Verif the dependencies. These are the files to delete. + # -- Verify the dependencies. These are the files to delete. file_names = [x.name for x in dependencies] assert file_names == ["aaa", "bbb", "zadig.ini", "_build"] diff --git a/test/scons/testing.py b/test/scons/testing.py index 442b6b26..755b9d4b 100644 --- a/test/scons/testing.py +++ b/test/scons/testing.py @@ -9,7 +9,7 @@ import SCons.Script.Main from google.protobuf import text_format from apio.scons.apio_env import ApioEnv -from apio.proto.apio_pb2 import SconsParams, TargetParams +from apio.common.proto.apio_pb2 import SconsParams, TargetParams # R0801: Similar lines in 2 files # pylint: disable=R0801 @@ -31,7 +31,7 @@ synth: false pnr: false } -envrionment { +environment { platform_id: "darwin_arm64" is_debug: true yosys_path: "/Users/user/.apio/packages/oss-cad-suite/share/yosys" @@ -45,13 +45,13 @@ class SconsHacks: - """A collection of staticmethods that encapsulate scons access outside of + """A collection of static methods that encapsulate scons access outside of the official scons API. Hopefully this will not be too difficult to adapt in future versions of SCons.""" @staticmethod def reset_scons_state() -> None: - """Reset the relevant SCons global variables. וUnfurtunally scons + """Reset the relevant SCons global variables. Unfortunately scons uses a few global variables to hold its state. This works well in normal operation where an scons process contains a single scons session but with pytest testing, where multiple independent tests @@ -82,7 +82,7 @@ def get_targets() -> Dict: def make_test_scons_params() -> SconsParams: - """Create a frake scons params for testing.""" + """Create a fake scons params for testing.""" return text_format.Parse(TEST_PARAMS, SconsParams()) @@ -90,12 +90,15 @@ def make_test_apio_env( *, targets: Optional[List[str]] = None, platform_id: str = None, + is_windows: bool = None, is_debug: bool = None, target_params: TargetParams = None, ) -> ApioEnv: """Creates a fresh apio env for testing. The env is created with the current directory as the root dir. """ + # -- Specify both or nether. + assert (platform_id is None) == (is_windows is None) # -- Bring scons to a starting state. SconsHacks.reset_scons_state() @@ -105,9 +108,11 @@ def make_test_apio_env( # -- Apply user overrides. if platform_id is not None: - scons_params.envrionment.platform_id = platform_id + scons_params.environment.platform_id = platform_id + if is_windows is not None: + scons_params.environment.is_windows = is_windows if is_debug is not None: - scons_params.envrionment.is_debug = is_debug + scons_params.environment.is_debug = is_debug if target_params is not None: scons_params.target.MergeFrom(target_params) diff --git a/test/test_apio_context.py b/test/test_apio_context.py index 8463452b..5d32c2bf 100644 --- a/test/test_apio_context.py +++ b/test/test_apio_context.py @@ -44,7 +44,7 @@ def _test_home_dir_with_a_bad_character( invalid_home_dir = sb.sandbox_dir / f"apio-{invalid_char}-home" os.environ["APIO_HOME_DIR"] = str(invalid_home_dir) - # -- Initialize an apio context. It shoudl exit with an error. + # -- Initialize an apio context. It should exit with an error. with raises(SystemExit) as e: ApioContext(scope=ApioContextScope.NO_PROJECT) assert e.value.code == 1 @@ -74,11 +74,11 @@ def test_home_dir_with_relative_path( invalid_home_dir = Path("./aa/bb") os.environ["APIO_HOME_DIR"] = str(invalid_home_dir) - # -- Initialize an apio context. It shoudl exit with an error. + # -- Initialize an apio context. It should exit with an error. with raises(SystemExit) as e: ApioContext(scope=ApioContextScope.NO_PROJECT) assert e.value.code == 1 assert ( - "Error: apio home dir should be an absolute path" + "Error: Apio home dir should be an absolute path" in capsys.readouterr().out ) diff --git a/tox.ini b/tox.ini index a6d56dc5..e31fccd6 100644 --- a/tox.ini +++ b/tox.ini @@ -36,7 +36,7 @@ isolated_build = True # Runs testenv:x for each env x here. Listing in increasing order -# since more compatibility errors happens with the olders version. +# since more compatibility errors happens with the older version. envlist = lint py39 @@ -62,14 +62,14 @@ setenv= test-boards # When we generate the proto files at apio/proto, we also patch at the top -# directives to supress pylint warnings. +# directives to suppress pylint warnings. # # The --prefer-stubs option is for the protocol buffers file, telling lint -# to use the definitions in the .pyi stubs istead of the criptic protocol +# to use the definitions in the .pyi stubs instead of the cryptic protocol # buffers .py files. commands = - black {env:LINT_ITEMS} --exclude apio/proto - flake8 {env:LINT_ITEMS} --exclude apio/proto + black {env:LINT_ITEMS} --exclude apio/common/proto + flake8 {env:LINT_ITEMS} --exclude apio/common/proto pylint {env:LINT_ITEMS} --prefer-stubs True # ----------------------------------------------------