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

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 # 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 """ 

106 

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: 

111 

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) 

114 

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) 

124 

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

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

127 

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

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

130 

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) 

140 

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

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

143 

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 ) 

152 

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) 

156 

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

158 if not cmd: 

159 sys.exit(0) 

160 

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

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

163 

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}:") 

168 

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) 

176 

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) 

181 

182 else: 

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

184 

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

186 sys.exit(exit_code)