diff options
Diffstat (limited to 'pre_commit/languages/rust.py')
-rw-r--r-- | pre_commit/languages/rust.py | 140 |
1 files changed, 105 insertions, 35 deletions
diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py index 01c3730..204f2aa 100644 --- a/pre_commit/languages/rust.py +++ b/pre_commit/languages/rust.py @@ -1,13 +1,17 @@ from __future__ import annotations import contextlib +import functools import os.path +import shutil +import sys +import tempfile +import urllib.request from typing import Generator from typing import Sequence -import toml - import pre_commit.constants as C +from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import Var @@ -16,40 +20,105 @@ from pre_commit.languages import helpers from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output_b +from pre_commit.util import make_executable +from pre_commit.util import win_exe ENVIRONMENT_DIR = 'rustenv' -get_default_version = helpers.basic_get_default_version health_check = helpers.basic_health_check -def get_env_patch(target_dir: str) -> PatchesT: +@functools.lru_cache(maxsize=1) +def get_default_version() -> str: + # If rust is already installed, we can save a bunch of setup time by + # using the installed version. + # + # Just detecting the executable does not suffice, because if rustup is + # installed but no toolchain is available, then `cargo` exists but + # cannot be used without installing a toolchain first. + if cmd_output_b('cargo', '--version', check=False)[0] == 0: + return 'system' + else: + return C.DEFAULT + + +def _rust_toolchain(language_version: str) -> str: + """Transform the language version into a rust toolchain version.""" + if language_version == C.DEFAULT: + return 'stable' + else: + return language_version + + +def _envdir(prefix: Prefix, version: str) -> str: + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + return prefix.path(directory) + + +def get_env_patch(target_dir: str, version: str) -> PatchesT: return ( + ('CARGO_HOME', target_dir), ('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))), + # Only set RUSTUP_TOOLCHAIN if we don't want use the system's default + # toolchain + *( + (('RUSTUP_TOOLCHAIN', _rust_toolchain(version)),) + if version != 'system' else () + ), ) @contextlib.contextmanager -def in_env(prefix: Prefix) -> Generator[None, None, None]: - target_dir = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) - with envcontext(get_env_patch(target_dir)): +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + with envcontext( + get_env_patch(_envdir(prefix, language_version), language_version), + ): yield def _add_dependencies( - cargo_toml_path: str, + prefix: Prefix, additional_dependencies: set[str], ) -> None: - with open(cargo_toml_path, 'r+') as f: - cargo_toml = toml.load(f) - cargo_toml.setdefault('dependencies', {}) - for dep in additional_dependencies: - name, _, spec = dep.partition(':') - cargo_toml['dependencies'][name] = spec or '*' - f.seek(0) - toml.dump(cargo_toml, f) - f.truncate() + crates = [] + for dep in additional_dependencies: + name, _, spec = dep.partition(':') + crate = f'{name}@{spec or "*"}' + crates.append(crate) + + helpers.run_setup_cmd(prefix, ('cargo', 'add', *crates)) + + +def install_rust_with_toolchain(toolchain: str) -> None: + with tempfile.TemporaryDirectory() as rustup_dir: + with envcontext((('RUSTUP_HOME', rustup_dir),)): + # acquire `rustup` if not present + if parse_shebang.find_executable('rustup') is None: + # We did not detect rustup and need to download it first. + if sys.platform == 'win32': # pragma: win32 cover + url = 'https://win.rustup.rs/x86_64' + else: # pragma: win32 no cover + url = 'https://sh.rustup.rs' + + resp = urllib.request.urlopen(url) + + rustup_init = os.path.join(rustup_dir, win_exe('rustup-init')) + with open(rustup_init, 'wb') as f: + shutil.copyfileobj(resp, f) + make_executable(rustup_init) + + # install rustup into `$CARGO_HOME/bin` + cmd_output_b( + rustup_init, '-y', '--quiet', '--no-modify-path', + '--default-toolchain', 'none', + ) + + cmd_output_b( + 'rustup', 'toolchain', 'install', '--no-self-update', + toolchain, + ) def install_environment( @@ -57,10 +126,7 @@ def install_environment( version: str, additional_dependencies: Sequence[str], ) -> None: - helpers.assert_version_default('rust', version) - directory = prefix.path( - helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), - ) + directory = _envdir(prefix, version) # There are two cases where we might want to specify more dependencies: # as dependencies for the library being built, and as binary packages @@ -77,24 +143,28 @@ def install_environment( } lib_deps = set(additional_dependencies) - cli_deps - if len(lib_deps) > 0: - _add_dependencies(prefix.path('Cargo.toml'), lib_deps) - with clean_path_on_failure(directory): packages_to_install: set[tuple[str, ...]] = {('--path', '.')} for cli_dep in cli_deps: cli_dep = cli_dep[len('cli:'):] - package, _, version = cli_dep.partition(':') - if version != '': - packages_to_install.add((package, '--version', version)) + package, _, crate_version = cli_dep.partition(':') + if crate_version != '': + packages_to_install.add((package, '--version', crate_version)) else: packages_to_install.add((package,)) - for args in packages_to_install: - cmd_output_b( - 'cargo', 'install', '--bins', '--root', directory, *args, - cwd=prefix.prefix_dir, - ) + with in_env(prefix, version): + if version != 'system': + install_rust_with_toolchain(_rust_toolchain(version)) + + if len(lib_deps) > 0: + _add_dependencies(prefix, lib_deps) + + for args in packages_to_install: + cmd_output_b( + 'cargo', 'install', '--bins', '--root', directory, *args, + cwd=prefix.prefix_dir, + ) def run_hook( @@ -102,5 +172,5 @@ def run_hook( file_args: Sequence[str], color: bool, ) -> tuple[int, bytes]: - with in_env(hook.prefix): + with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) |