Coverage for apio/commands/apio_raw.py: 80%
46 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +0000
1# -*- coding: utf-8 -*-
2# -- This file is part of the Apio project
3# -- (C) 2016-2024 FPGAwars
4# -- Authors
5# -- * Jesús Arroyo (2016-2019)
6# -- * Juan Gonzalez (obijuan) (2019-2024)
7# -- License GPLv2
8"""Implementation of 'apio raw' command"""
10import sys
11import shlex
12import subprocess
13import click
14from apio.common.apio_console import cout, fatal_error
15from apio.common.apio_styles import SUCCESS, ERROR
16from apio.apio_context import (
17 ApioContext,
18 PackagesPolicy,
19 ProjectPolicy,
20 RemoteConfigPolicy,
21)
22from apio.commands import options
23from apio.utils import cmd_util
24from apio.utils.cmd_util import ApioCommand
26# ----------- apio raw
29def _run_raw_command(
30 arg_list: list[str], is_windows: bool, verbose: bool
31) -> int:
32 """
33 Runs a command and returns its exit code.
35 This functionality has a few special requeiements so we placed it in a
36 method of its own despite its simplicity. If you change it for any reason,
37 make sure that your new code doesn't break anything.
39 Requirements:
41 1. Users should be able to copy commands such as yosys and nextpnr
42 that are printed by apio scons and paste them with no change after
43 `apio raw --` for proper execution. This applies to the native shell
44 of each platform (e.g. on windows cmd or powershell rather than gitbash)
46 2. On window, commands such as 'apio raw -- zadig' will handle privilege
47 elevation properly and will ask the user for approval.
49 3. On windows, commands such as `apio raw -- xyz` should match also the
50 files xyz.exe, xyz.cmd and xyz.bat.
51 """
52 if not arg_list: 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true
53 return 0 # nothing to run
55 # -- Join the args with proper quotes for the native shell.
56 if is_windows: 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true
57 cmd_str = subprocess.list2cmdline(arg_list)
58 else:
59 cmd_str = shlex.join(arg_list)
61 # -- Run the command and return the exit code.
62 if verbose: 62 ↛ 63line 62 didn't jump to line 63 because the condition on line 62 was never true
63 cout(f"\n---- Executing: [{cmd_str}]")
65 return subprocess.call(cmd_str, shell=True)
68# -- Text in the rich-text format of the python rich library.
69APIO_RAW_HELP = """
70The command 'apio raw' allows you to bypass Apio and run underlying tools \
71directly. This is an advanced command that requires familiarity with the \
72underlying tools.
74Before running the command, Apio temporarily modifies system environment \
75variables such as '$PATH' to provide access to its packages. To view these \
76environment changes, run the command with the '-v' option.
78Examples:[code]
79 apio raw -- yosys --version # Yosys version
80 apio raw -v -- yosys --version # Verbose apio info.
81 apio raw -- yosys # Yosys interactive mode.
82 apio raw -- icepll -i 12 -o 30 # Calc ICE PLL.
83 apio raw -- which yosys # Lookup a command.
84 apio raw -- bash # Open a shell with Apio's env.
85 apio raw -- zadig # Run Zadig (on Windows).
86 apio raw -v # Show apio env setting.
87 apio raw -h # Show this help info.[/code]
89The marker '--' must separate between the arguments of the apio \
90command itself and those of the executed command.
91"""
94@click.command(
95 name="raw",
96 cls=ApioCommand,
97 short_help="Execute commands directly from the Apio packages.",
98 help=APIO_RAW_HELP,
99 context_settings={"ignore_unknown_options": True},
100)
101@click.pass_context
102@click.argument("cmd", metavar="COMMAND", nargs=-1, type=click.UNPROCESSED)
103@options.verbose_option
104def cli(
105 cmd_ctx: click.Context,
106 *,
107 # Arguments
108 cmd: tuple[str],
109 # Options
110 verbose: bool,
111):
112 """Implements the apio raw command which executes user
113 specified commands from apio installed tools.
114 """
116 # -- If the user specifies a raw command, verify that the '--' separator
117 # -- exists and that all the command tokens were specified after it.
118 # -- Ideally Click should be able to validate it but it doesn't (?).
119 if cmd:
121 # -- Locate the first '--' in argv. None if not found.
122 dd_index = next((i for i, x in enumerate(sys.argv) if x == "--"), None)
124 # -- If the '--' separator was not specified this is an error.
125 if dd_index is None:
126 fatal_error(
127 "The raw command separator '--' was not found.",
128 info=[
129 "The raw command should be specified after "
130 + "a '--' separator.",
131 "Type 'apio raw -h' for details.",
132 ],
133 )
135 # -- Number of command tokens after the "--"
136 n_after = len(sys.argv) - dd_index - 1
138 # -- Command tokens that where specified before the '--'
139 tokens_before = list(cmd)[: len(cmd) - n_after]
141 # -- Should have no command tokens before the "--"
142 if tokens_before:
143 fatal_error(
144 f"Invalid arguments: {tokens_before}.",
145 info=[
146 "Did you mean to have them after the '--' separator?",
147 "See 'apio raw -h' for details.",
148 ],
149 )
151 # -- At lease one of -v and cmd should be specified.
152 cmd_util.check_at_least_one_param(cmd_ctx, ["verbose", "cmd"])
154 # -- Create an apio context. We don't care about an apio project.
155 # -- Using config and packages because we want the binaries in the apio
156 # -- packages to be available for the 'apio raw' command.
157 apio_ctx = ApioContext(
158 project_policy=ProjectPolicy.NO_PROJECT,
159 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
160 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
161 )
163 # -- Set the env for packages. If verbose, also dumping the env changes
164 # -- in a user friendly way.
165 apio_ctx.set_env_for_packages(quiet=not verbose, verbose=verbose)
167 # -- If no command, we are done.
168 if not cmd:
169 return
171 # -- Convert the tuple of strings to a list of strings.
172 arg_list: list[str] = list(cmd)
174 # -- Invoke the command.
175 exit_code = _run_raw_command(arg_list, apio_ctx.is_windows, verbose)
177 if verbose: 177 ↛ 178line 177 didn't jump to line 178 because the condition on line 177 was never true
178 cout("----\n")
179 if exit_code == 0:
180 cout("Exit status [0] OK", style=SUCCESS)
182 else:
183 cout(f"Exist status [{exit_code}] ERROR", style=ERROR)
185 # -- Return the command's status code.
186 sys.exit(exit_code)