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

58 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-25 02:31 +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""" 

9 

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 

26 

27# ----------- apio raw 

28 

29 

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 

41 

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) 

46 

47 # Mac and linux. 

48 return subprocess.call(arg_list, shell=False) 

49 

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 

54 

55 except PermissionError: 

56 cout(f"Error: Permission denied → {arg_list[0]}", style=ERROR) 

57 return 126 

58 

59 

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. 

65 

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. 

69 

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] 

80 

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

82command itself and those of the executed command. 

83""" 

84 

85 

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 *, 

99 # Arguments 

100 cmd: Tuple[str], 

101 # Options 

102 verbose: bool, 

103): 

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

105 specified commands from apio installed tools. 

106 """ 

107 

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

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

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

111 if cmd: 

112 

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

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

115 

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

117 if dd_index is None: 

118 cerror("The raw command separator '--' was not found.") 

119 cout( 

120 "The raw command should be specified after a '--' separator.", 

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

122 style=INFO, 

123 ) 

124 sys.exit(1) 

125 

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

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

128 

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

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

131 

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

133 if tokens_before: 

134 cerror(f"Invalid arguments: {tokens_before}.") 

135 cout( 

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

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

138 style=INFO, 

139 ) 

140 sys.exit(1) 

141 

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

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

144 

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

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

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

148 apio_ctx = ApioContext( 

149 project_policy=ProjectPolicy.NO_PROJECT, 

150 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

151 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

152 ) 

153 

154 # -- Set the env for packages. If verbose, also dumping the env changes 

155 # -- in a user friendly way. 

156 apio_ctx.set_env_for_packages(quiet=not verbose, verbose=verbose) 

157 

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

159 if not cmd: 

160 sys.exit(0) 

161 

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

163 cmd: List[str] = list(cmd) 

164 

165 # -- Echo the commands. The apio raw command is platform dependent 

166 # -- so this may help us and the user diagnosing issues. 

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

168 cout(f"\n---- Executing {cmd}:") 

169 

170 # -- Invoke the command. 

171 # try: 

172 # exit_code = subprocess.call(cmd, shell=False) 

173 exit_code = run_command_with_possible_elevation(apio_ctx, cmd) 

174 # except FileNotFoundError as e: 

175 # cout(f"{e}", style=ERROR) 

176 # sys.exit(1) 

177 

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

179 cout("----\n") 

180 if exit_code == 0: 

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

182 

183 else: 

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

185 

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

187 sys.exit(exit_code)