11from __future__ import annotations
22
33import contextlib
4+ import functools
45import os .path
6+ import platform
7+ import shutil
8+ import sys
9+ import tempfile
10+ import urllib .request
511from typing import Generator
612from typing import Sequence
713
814import toml
915
1016import pre_commit .constants as C
17+ from pre_commit import parse_shebang
1118from pre_commit .envcontext import envcontext
1219from pre_commit .envcontext import PatchesT
1320from pre_commit .envcontext import Var
1623from pre_commit .prefix import Prefix
1724from pre_commit .util import clean_path_on_failure
1825from pre_commit .util import cmd_output_b
26+ from pre_commit .util import make_executable
27+ from pre_commit .util import win_exe
1928
2029ENVIRONMENT_DIR = 'rustenv'
21- get_default_version = helpers .basic_get_default_version
2230health_check = helpers .basic_health_check
2331
2432
25- def get_env_patch (target_dir : str ) -> PatchesT :
33+ @functools .lru_cache (maxsize = 1 )
34+ def get_default_version () -> str :
35+ # If rust is already installed, we can save a bunch of setup time by
36+ # using the installed version.
37+ #
38+ # Just detecting the executable does not suffice, because if rustup is
39+ # installed but no toolchain is available, then `cargo` exists but
40+ # cannot be used without installing a toolchain first.
41+ if cmd_output_b ('cargo' , '--version' , retcode = None )[0 ] == 0 :
42+ return 'system'
43+ else :
44+ return C .DEFAULT
45+
46+
47+ def _rust_toolchain (language_version : str ) -> str :
48+ """Transform the language version into a rust toolchain version."""
49+ if language_version == C .DEFAULT :
50+ return 'stable'
51+ else :
52+ return language_version
53+
54+
55+ def _envdir (prefix : Prefix , version : str ) -> str :
56+ directory = helpers .environment_dir (ENVIRONMENT_DIR , version )
57+ return prefix .path (directory )
58+
59+
60+ def get_env_patch (target_dir : str , version : str ) -> PatchesT :
2661 return (
62+ ('CARGO_HOME' , target_dir ),
2763 ('PATH' , (os .path .join (target_dir , 'bin' ), os .pathsep , Var ('PATH' ))),
64+ # Only set RUSTUP_TOOLCHAIN if we don't want use the system's default
65+ # toolchain
66+ * (
67+ (('RUSTUP_TOOLCHAIN' , _rust_toolchain (version )),)
68+ if version != 'system' else ()
69+ ),
2870 )
2971
3072
3173@contextlib .contextmanager
32- def in_env (prefix : Prefix ) -> Generator [None , None , None ]:
33- target_dir = prefix .path (
34- helpers .environment_dir (ENVIRONMENT_DIR , C .DEFAULT ),
35- )
36- with envcontext (get_env_patch (target_dir )):
74+ def in_env (
75+ prefix : Prefix ,
76+ language_version : str ,
77+ ) -> Generator [None , None , None ]:
78+ with envcontext (
79+ get_env_patch (_envdir (prefix , language_version ), language_version ),
80+ ):
3781 yield
3882
3983
@@ -52,15 +96,45 @@ def _add_dependencies(
5296 f .truncate ()
5397
5498
99+ def install_rust_with_toolchain (toolchain : str ) -> None :
100+ with tempfile .TemporaryDirectory () as rustup_dir :
101+ with envcontext ((('RUSTUP_HOME' , rustup_dir ),)):
102+ # acquire `rustup` if not present
103+ if parse_shebang .find_executable ('rustup' ) is None :
104+ # We did not detect rustup and need to download it first.
105+ if sys .platform == 'win32' : # pragma: win32 cover
106+ if platform .machine () == 'x86_64' :
107+ url = 'https://win.rustup.rs/x86_64'
108+ else :
109+ url = 'https://win.rustup.rs/i686'
110+ else : # pragma: win32 no cover
111+ url = 'https://sh.rustup.rs'
112+
113+ resp = urllib .request .urlopen (url )
114+
115+ rustup_init = os .path .join (rustup_dir , win_exe ('rustup-init' ))
116+ with open (rustup_init , 'wb' ) as f :
117+ shutil .copyfileobj (resp , f )
118+ make_executable (rustup_init )
119+
120+ # install rustup into `$CARGO_HOME/bin`
121+ cmd_output_b (
122+ rustup_init , '-y' , '--quiet' , '--no-modify-path' ,
123+ '--default-toolchain' , 'none' ,
124+ )
125+
126+ cmd_output_b (
127+ 'rustup' , 'toolchain' , 'install' , '--no-self-update' ,
128+ toolchain ,
129+ )
130+
131+
55132def install_environment (
56133 prefix : Prefix ,
57134 version : str ,
58135 additional_dependencies : Sequence [str ],
59136) -> None :
60- helpers .assert_version_default ('rust' , version )
61- directory = prefix .path (
62- helpers .environment_dir (ENVIRONMENT_DIR , C .DEFAULT ),
63- )
137+ directory = _envdir (prefix , version )
64138
65139 # There are two cases where we might want to specify more dependencies:
66140 # as dependencies for the library being built, and as binary packages
@@ -84,23 +158,27 @@ def install_environment(
84158 packages_to_install : set [tuple [str , ...]] = {('--path' , '.' )}
85159 for cli_dep in cli_deps :
86160 cli_dep = cli_dep [len ('cli:' ):]
87- package , _ , version = cli_dep .partition (':' )
88- if version != '' :
89- packages_to_install .add ((package , '--version' , version ))
161+ package , _ , crate_version = cli_dep .partition (':' )
162+ if crate_version != '' :
163+ packages_to_install .add ((package , '--version' , crate_version ))
90164 else :
91165 packages_to_install .add ((package ,))
92166
93- for args in packages_to_install :
94- cmd_output_b (
95- 'cargo' , 'install' , '--bins' , '--root' , directory , * args ,
96- cwd = prefix .prefix_dir ,
97- )
167+ with in_env (prefix , version ):
168+ if version != 'system' :
169+ install_rust_with_toolchain (_rust_toolchain (version ))
170+
171+ for args in packages_to_install :
172+ cmd_output_b (
173+ 'cargo' , 'install' , '--bins' , '--root' , directory , * args ,
174+ cwd = prefix .prefix_dir ,
175+ )
98176
99177
100178def run_hook (
101179 hook : Hook ,
102180 file_args : Sequence [str ],
103181 color : bool ,
104182) -> tuple [int , bytes ]:
105- with in_env (hook .prefix ):
183+ with in_env (hook .prefix , hook . language_version ):
106184 return helpers .run_xargs (hook , hook .cmd , file_args , color = color )
0 commit comments