Coverage for apio / commands / apio_raw.py: 74%
58 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-24 01:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-24 01: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 subprocess
12import shlex
13from typing import Tuple, List
14import click
15from apio.common.apio_console import cout, cerror
16from apio.common.apio_styles import SUCCESS, ERROR, INFO
17from apio.apio_context import (
18 ApioContext,
19 PackagesPolicy,
20 ProjectPolicy,
21 RemoteConfigPolicy,
22)
23from apio.commands import options
24from apio.utils import cmd_util
25from apio.utils.cmd_util import ApioCommand
27# ----------- apio raw
30def run_command_with_possible_elevation(
31 apio_ctx: ApioContext, arg_list: List[str]
32) -> int:
33 """
34 Runs a command and returns its exit code.
35 On Windows: allows UAC elevation (like os.system), e.g. for zadig.
36 On macOS/Linux: runs directly
37 Never raises — always returns an int (even on catastrophic errors.
38 """
39 if not arg_list: 39 ↛ 40line 39 didn't jump to line 40 because the condition on line 39 was never true
40 return 0 # nothing to run
42 try:
43 if apio_ctx.is_windows: 43 ↛ 44line 43 didn't jump to line 44 because the condition on line 43 was never true
44 cmd_line = " ".join(shlex.quote(arg) for arg in arg_list)
45 return subprocess.call(["cmd.exe", "/c", cmd_line], shell=False)
47 # Mac and linux.
48 return subprocess.call(arg_list, shell=False)
50 # Specific common errors — give user-friendly feedback
51 except FileNotFoundError:
52 cout(f"Error: Command not found → {arg_list[0]}", style=ERROR)
53 return 127
55 except PermissionError:
56 cout(f"Error: Permission denied → {arg_list[0]}", style=ERROR)
57 return 126
60# -- Text in the rich-text format of the python rich library.
61APIO_RAW_HELP = """
62The command 'apio raw' allows you to bypass Apio and run underlying tools \
63directly. This is an advanced command that requires familiarity with the \
64underlying tools.
66Before running the command, Apio temporarily modifies system environment \
67variables such as '$PATH' to provide access to its packages. To view these \
68environment changes, run the command with the '-v' option.
70Examples:[code]
71 apio raw -- yosys --version # Yosys version
72 apio raw -v -- yosys --version # Verbose apio info.
73 apio raw -- yosys # Yosys interactive mode.
74 apio raw -- icepll -i 12 -o 30 # Calc ICE PLL.
75 apio raw -- which yosys # Lookup a command.
76 apio raw -- bash # Open a shell with Apio's env.
77 apio raw -- zadig # Run Zadig (on Windows).
78 apio raw -v # Show apio env setting.
79 apio raw -h # Show this help info.[/code]
81The marker '--' must separate between the arguments of the apio \
82command itself and those of the executed command.
83"""
86@click.command(
87 name="raw",
88 cls=ApioCommand,
89 short_help="Execute commands directly from the Apio packages.",
90 help=APIO_RAW_HELP,
91 context_settings={"ignore_unknown_options": True},
92)
93@click.pass_context
94@click.argument("cmd", metavar="COMMAND", nargs=-1, type=click.UNPROCESSED)
95@options.verbose_option
96def cli(
97 cmd_ctx: click.Context,
98 # Arguments
99 cmd: Tuple[str],
100 # Options
101 verbose: bool,
102):
103 """Implements the apio raw command which executes user
104 specified commands from apio installed tools.
105 """
107 # -- If the user specifies a raw command, verify that the '--' separator
108 # -- exists and that all the command tokens were specified after it.
109 # -- Ideally Click should be able to validate it but it doesn't (?).
110 if cmd:
112 # -- Locate the first '--' in argv. None if not found.
113 dd_index = next((i for i, x in enumerate(sys.argv) if x == "--"), None)
115 # -- If the '--' separator was not specified this is an error.
116 if dd_index is None:
117 cerror("The raw command separator '--' was not found.")
118 cout(
119 "The raw command should be specified after a '--' separator.",
120 "Type 'apio raw -h' for details.",
121 style=INFO,
122 )
123 sys.exit(1)
125 # -- Number of command tokens after the "--"
126 n_after = len(sys.argv) - dd_index - 1
128 # -- Command tokens that where specified before the '--'
129 tokens_before = list(cmd)[: len(cmd) - n_after]
131 # -- Should have no command tokens before the "--"
132 if tokens_before:
133 cerror(f"Invalid arguments: {tokens_before}.")
134 cout(
135 "Did you mean to have them after the '--' separator?",
136 "See 'apio raw -h' for details.",
137 style=INFO,
138 )
139 sys.exit(1)
141 # -- At lease one of -v and cmd should be specified.
142 cmd_util.check_at_least_one_param(cmd_ctx, ["verbose", "cmd"])
144 # -- Create an apio context. We don't care about an apio project.
145 # -- Using config and packages because we want the binaries in the apio
146 # -- packages to be available for the 'apio raw' command.
147 apio_ctx = ApioContext(
148 project_policy=ProjectPolicy.NO_PROJECT,
149 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
150 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
151 )
153 # -- Set the env for packages. If verbose, also dumping the env changes
154 # -- in a user friendly way.
155 apio_ctx.set_env_for_packages(quiet=not verbose, verbose=verbose)
157 # -- If no command, we are done.
158 if not cmd:
159 sys.exit(0)
161 # -- Convert the tuple of strings to a list of strings.
162 cmd: List[str] = list(cmd)
164 # -- Echo the commands. The apio raw command is platform dependent
165 # -- so this may help us and the user diagnosing issues.
166 if verbose: 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true
167 cout(f"\n---- Executing {cmd}:")
169 # -- Invoke the command.
170 # try:
171 # exit_code = subprocess.call(cmd, shell=False)
172 exit_code = run_command_with_possible_elevation(apio_ctx, cmd)
173 # except FileNotFoundError as e:
174 # cout(f"{e}", style=ERROR)
175 # sys.exit(1)
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)