1
1
import logging
2
+ from os import environ
3
+ from shutil import which
4
+ import platform
5
+ import re
2
6
import sys
3
7
import nox
4
8
10
14
11
15
nox .options .default_venv_backend = "uv"
12
16
17
+ BIN_STALL_CARGO = re .compile (r"^([^\s]+)\s([^:]+):" )
18
+ CARGO_BINSTALL = "cargo-binstall"
19
+ IS_CI = "CI" in environ
20
+
21
+
22
+ class CargoInstaller :
23
+ def __init__ (self , session : nox .Session ):
24
+ cargo_installed : str | None = session .run (
25
+ "cargo" , "install" , "--list" , external = True , silent = True
26
+ )
27
+ assert cargo_installed is not None , "Is rust cargo installed?"
28
+ cargo_bins : dict [str , str ] = {}
29
+ for line in cargo_installed .splitlines ():
30
+ found = BIN_STALL_CARGO .match (line )
31
+ if found is not None :
32
+ name , version = found .groups ()
33
+ cargo_bins .setdefault (name , version )
34
+ self .cargo_bins = cargo_bins
35
+ if IS_CI :
36
+ for dep , ver in cargo_bins .items ():
37
+ ci_logger .info ("cargo installed %s: %s" % (dep , ver ))
38
+ self .cargo_install_cmd : tuple [str , ...] = ("cargo" , "install" )
39
+
40
+ path = which (CARGO_BINSTALL )
41
+
42
+ if CARGO_BINSTALL in cargo_bins :
43
+ ci_logger .info (
44
+ "Found %s: %s" % (CARGO_BINSTALL , cargo_bins [CARGO_BINSTALL ])
45
+ )
46
+ elif path is not None :
47
+ ci_logger .info ("Found: %s in %s" % (CARGO_BINSTALL , path ))
48
+ elif not IS_CI :
49
+ ci_logger .info ("Installing %s" % CARGO_BINSTALL )
50
+ match platform .system ():
51
+ case "Windows" :
52
+ one_liner = """Set-ExecutionPolicy Unrestricted -Scope Process; iex (iwr "https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.ps1").Content"""
53
+ session .run (* one_liner .split (), external = True )
54
+ case "Linux" | "Darwin" :
55
+ one_liner = "curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash"
56
+ session .run (* one_liner .split (), external = True )
57
+ case _:
58
+ session .run (
59
+ * self .cargo_install_cmd ,
60
+ CARGO_BINSTALL ,
61
+ "--locked" ,
62
+ external = True ,
63
+ )
64
+ self .cargo_install_cmd = ("cargo" , "binstall" , "-y" )
65
+
66
+ def check_install (self , req : str , session : nox .Session ):
67
+ """Use cargo to ensure `req` is installed.
68
+
69
+ Parameters:
70
+ req: The package name (on crates.io) to check and install.
71
+ Supports explicit version in the form
72
+
73
+ Typically this is the project's name as published on crates.io.
74
+ """
75
+
76
+ ver = None
77
+ if "@" in req :
78
+ req , ver = req .split ("@" , 1 )[:2 ]
79
+
80
+ def install ():
81
+ ci_logger .info ("Installing %s" % req )
82
+ dep = req if not ver else f"{ req } @{ ver } "
83
+ session .run (* self .cargo_install_cmd , dep , "--locked" , external = True )
84
+
85
+ path = which (req )
86
+
87
+ installed = False
88
+ if req in self .cargo_bins :
89
+ ci_logger .info ("Found %s %s" % (req , self .cargo_bins [req ]))
90
+ installed = True
91
+ elif path is not None :
92
+ ci_logger .info ("Found: %s in %s" % (req , path ))
93
+ installed = True
94
+ if ver or not installed :
95
+ install ()
96
+
13
97
14
98
def uv_sync (session : nox .Session , * args : str ):
15
99
session .run_install (
@@ -79,6 +163,13 @@ def test(session: nox.Session):
79
163
Otherwise, the default profile is used.
80
164
"""
81
165
uv_sync (session , "--group" , "test" )
166
+ installer = CargoInstaller (session )
167
+ for req in [
168
+ "cargo-llvm-cov" ,
169
+ "cargo-nextest" ,
170
+ ]:
171
+ installer .check_install (req , session )
172
+
82
173
session .run (
83
174
"cargo" ,
84
175
"llvm-cov" ,
@@ -102,6 +193,8 @@ def test_clean(session: nox.Session):
102
193
This is useful if coverage data needs to be
103
194
completely refreshed.
104
195
"""
196
+ installer = CargoInstaller (session )
197
+ installer .check_install ("cargo-llvm-cov" , session )
105
198
session .run ("cargo" , "llvm-cov" , "clean" , external = True )
106
199
107
200
@@ -114,6 +207,8 @@ def llvm_cov(session: nox.Session):
114
207
115
208
Otherwise, the default profile is used.
116
209
"""
210
+ installer = CargoInstaller (session )
211
+ installer .check_install ("cargo-llvm-cov" , session )
117
212
session .run (
118
213
"cargo" ,
119
214
"llvm-cov" ,
@@ -135,6 +230,9 @@ def pretty_cov(session: nox.Session):
135
230
136
231
Otherwise, the default profile is used.
137
232
"""
233
+ installer = CargoInstaller (session )
234
+ for req in ["cargo-llvm-cov" , "llvm-cov-pretty" ]:
235
+ installer .check_install (req , session )
138
236
session .run (
139
237
"cargo" ,
140
238
"llvm-cov" ,
@@ -156,6 +254,8 @@ def lcov(session: nox.Session):
156
254
Useful for codecov uploads and VSCode extensions
157
255
like "Coverage Gutters".
158
256
"""
257
+ installer = CargoInstaller (session )
258
+ installer .check_install ("cargo-llvm-cov" , session )
159
259
session .run (
160
260
"cargo" ,
161
261
"llvm-cov" ,
@@ -177,3 +277,13 @@ def lint(session: nox.Session):
177
277
"cargo" , "clippy" , "--allow-staged" , "--allow-dirty" , "--fix" , external = True
178
278
)
179
279
session .run ("cargo" , "fmt" , external = True )
280
+
281
+
282
+ @nox .session (python = False )
283
+ def install (session : nox .Session ):
284
+ """Install necessary software in global env"""
285
+
286
+ installer = CargoInstaller (session )
287
+
288
+ for req in session .posargs :
289
+ installer .check_install (req , session )
0 commit comments