11import logging
2+ from os import environ
3+ from shutil import which
4+ import platform
5+ import re
26import sys
37import nox
48
1014
1115nox .options .default_venv_backend = "uv"
1216
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+ `cargo-nextest@0.9.77`.
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+
1397
1498def uv_sync (session : nox .Session , * args : str ):
1599 session .run_install (
@@ -79,6 +163,13 @@ def test(session: nox.Session):
79163 Otherwise, the default profile is used.
80164 """
81165 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+
82173 session .run (
83174 "cargo" ,
84175 "llvm-cov" ,
@@ -102,6 +193,8 @@ def test_clean(session: nox.Session):
102193 This is useful if coverage data needs to be
103194 completely refreshed.
104195 """
196+ installer = CargoInstaller (session )
197+ installer .check_install ("cargo-llvm-cov" , session )
105198 session .run ("cargo" , "llvm-cov" , "clean" , external = True )
106199
107200
@@ -114,6 +207,8 @@ def llvm_cov(session: nox.Session):
114207
115208 Otherwise, the default profile is used.
116209 """
210+ installer = CargoInstaller (session )
211+ installer .check_install ("cargo-llvm-cov" , session )
117212 session .run (
118213 "cargo" ,
119214 "llvm-cov" ,
@@ -135,6 +230,9 @@ def pretty_cov(session: nox.Session):
135230
136231 Otherwise, the default profile is used.
137232 """
233+ installer = CargoInstaller (session )
234+ for req in ["cargo-llvm-cov" , "llvm-cov-pretty" ]:
235+ installer .check_install (req , session )
138236 session .run (
139237 "cargo" ,
140238 "llvm-cov" ,
@@ -156,6 +254,8 @@ def lcov(session: nox.Session):
156254 Useful for codecov uploads and VSCode extensions
157255 like "Coverage Gutters".
158256 """
257+ installer = CargoInstaller (session )
258+ installer .check_install ("cargo-llvm-cov" , session )
159259 session .run (
160260 "cargo" ,
161261 "llvm-cov" ,
@@ -177,3 +277,13 @@ def lint(session: nox.Session):
177277 "cargo" , "clippy" , "--allow-staged" , "--allow-dirty" , "--fix" , external = True
178278 )
179279 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