Coverage for apio/commands/apio_raw.py: 80%

48 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-07 05:08 +0000

1# This file is part of the Apio project 

2# (C) 2016-present FPGAwars 

3# Project authors: 

4# * Jesús Arroyo (Jesus89) (2016-2019) 

5# * Juan Gonzalez (obijuan) (2019-2024) 

6# * The Apio Team (2024-present) 

7# License: GPLv2 

8 

9"""Implementation of 'apio raw' command""" 

10 

11import sys 

12import shlex 

13import subprocess 

14import click 

15from apio.common.apio_console import cout, fatal_error 

16from apio.common.apio_styles import SUCCESS, ERROR 

17from apio.apio_context import ( 

18 ApioContext, 

19 PackagesPolicy, 

20 ProjectPolicy, 

21 RemoteConfigPolicy, 

22) 

23from apio.common.common_util import cmd_in_env 

24from apio.commands import options 

25from apio.utils import cmd_util 

26from apio.utils.cmd_util import ApioCommand 

27 

28# ----------- apio raw 

29 

30 

31def _run_raw_command( 

32 arg_list: list[str], is_windows: bool, verbose: bool, env: dict[str, str] 

33) -> int: 

34 """ 

35 Runs a command and returns its exit code. 

36 

37 This functionality has a few special requeiements so we placed it in a 

38 method of its own despite its simplicity. If you change it for any reason, 

39 make sure that your new code doesn't break anything. 

40 

41 Requirements: 

42 

43 1. Users should be able to copy commands such as yosys and nextpnr 

44 that are printed by apio scons and paste them with no change after 

45 `apio raw --` for proper execution. This applies to the native shell 

46 of each platform (e.g. on windows cmd or powershell rather than gitbash) 

47 

48 2. On window, commands such as 'apio raw -- zadig' will handle privilege 

49 elevation properly and will ask the user for approval. 

50 

51 3. On windows, commands such as `apio raw -- xyz` should match also the 

52 files xyz.exe, xyz.cmd and xyz.bat. 

53 """ 

54 if not arg_list: 54 ↛ 55line 54 didn't jump to line 55 because the condition on line 54 was never true

55 return 0 # nothing to run 

56 

57 # -- Use a absolute path based on env's PATH. 

58 arg_list = cmd_in_env(arg_list, env, strict=False) 

59 

60 # -- Join the args with proper quotes for the native shell. 

61 if is_windows: 61 ↛ 62line 61 didn't jump to line 62 because the condition on line 61 was never true

62 cmd_str = subprocess.list2cmdline(arg_list) 

63 else: 

64 cmd_str = shlex.join(arg_list) 

65 

66 # -- Run the command and return the exit code. 

67 if verbose: 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true

68 cout(f"\n---- Executing: [{cmd_str}]") 

69 

70 return subprocess.call(cmd_str, shell=True, env=env) 

71 

72 

73# -- Text in the rich-text format of the python rich library. 

74APIO_RAW_HELP = """ 

75The command 'apio raw' allows you to bypass Apio and run underlying tools \ 

76directly. This is an advanced command that requires familiarity with the \ 

77underlying tools. 

78 

79Before running the command, Apio temporarily modifies system environment \ 

80variables such as '$PATH' to provide access to its packages. To view these \ 

81environment changes, run the command with the '-v' option. 

82 

83Examples:[code] 

84 apio raw -- yosys --version # Yosys version 

85 apio raw -v -- yosys --version # Verbose apio info. 

86 apio raw -- yosys # Yosys interactive mode. 

87 apio raw -- icepll -i 12 -o 30 # Calc ICE PLL. 

88 apio raw -- which yosys # Lookup a command. 

89 apio raw -- bash # Open a shell with Apio's env. 

90 apio raw -- zadig # Run Zadig (on Windows). 

91 apio raw -v # Show apio env setting. 

92 apio raw -h # Show this help info.[/code] 

93 

94The marker '--' must separate between the arguments of the apio \ 

95command itself and those of the executed command. 

96""" 

97 

98 

99@click.command( 

100 name="raw", 

101 cls=ApioCommand, 

102 short_help="Execute commands directly from the Apio packages.", 

103 help=APIO_RAW_HELP, 

104 context_settings={"ignore_unknown_options": True}, 

105) 

106@click.pass_context 

107@click.argument("cmd", metavar="COMMAND", nargs=-1, type=click.UNPROCESSED) 

108@options.verbose_option 

109def cli( 

110 cmd_ctx: click.Context, 

111 *, 

112 # Arguments 

113 cmd: tuple[str], 

114 # Options 

115 verbose: bool, 

116): 

117 """Implements the apio raw command which executes user 

118 specified commands from apio installed tools. 

119 """ 

120 

121 # -- If the user specifies a raw command, verify that the '--' separator 

122 # -- exists and that all the command tokens were specified after it. 

123 # -- Ideally Click should be able to validate it but it doesn't (?). 

124 if cmd: 

125 

126 # -- Locate the first '--' in argv. None if not found. 

127 dd_index = next((i for i, x in enumerate(sys.argv) if x == "--"), None) 

128 

129 # -- If the '--' separator was not specified this is an error. 

130 if dd_index is None: 

131 fatal_error( 

132 "The raw command separator '--' was not found.", 

133 info=[ 

134 "The raw command should be specified after " 

135 + "a '--' separator.", 

136 "Type 'apio raw -h' for details.", 

137 ], 

138 ) 

139 

140 # -- Number of command tokens after the "--" 

141 n_after = len(sys.argv) - dd_index - 1 

142 

143 # -- Command tokens that where specified before the '--' 

144 tokens_before = list(cmd)[: len(cmd) - n_after] 

145 

146 # -- Should have no command tokens before the "--" 

147 if tokens_before: 

148 fatal_error( 

149 f"Invalid arguments: {tokens_before}.", 

150 info=[ 

151 "Did you mean to have them after the '--' separator?", 

152 "See 'apio raw -h' for details.", 

153 ], 

154 ) 

155 

156 # -- At lease one of -v and cmd should be specified. 

157 cmd_util.check_at_least_one_param(cmd_ctx, ["verbose", "cmd"]) 

158 

159 # -- Create an apio context. We don't care about an apio project. 

160 # -- Using config and packages because we want the binaries in the apio 

161 # -- packages to be available for the 'apio raw' command. 

162 apio_ctx = ApioContext( 

163 project_policy=ProjectPolicy.NO_PROJECT, 

164 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

165 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

166 ) 

167 

168 # -- Set the env for the tools. If verbose, also dumping the env changes 

169 # -- in a user friendly way. 

170 tools_env = apio_ctx.get_env_for_subprocess( 

171 include_apio_packages=True, 

172 quiet=not verbose, 

173 verbose=verbose, 

174 ) 

175 

176 # -- If no command, we are done. 

177 if not cmd: 

178 return 

179 

180 # -- Convert the tuple of strings to a list of strings. 

181 arg_list: list[str] = list(cmd) 

182 

183 # -- Invoke the command. 

184 exit_code = _run_raw_command( 

185 arg_list, 

186 apio_ctx.is_windows, 

187 verbose, 

188 tools_env, 

189 ) 

190 

191 if verbose: 191 ↛ 192line 191 didn't jump to line 192 because the condition on line 191 was never true

192 cout("----\n") 

193 if exit_code == 0: 

194 cout("Exit status [0] OK", style=SUCCESS) 

195 

196 else: 

197 cout(f"Exist status [{exit_code}] ERROR", style=ERROR) 

198 

199 # -- Return the command's status code. 

200 sys.exit(exit_code)