Skip to content

Commit

Permalink
rust: Implement Rust installation using rustup/rustup-init
Browse files Browse the repository at this point in the history
This adds 1st-class language support for Rust in pre-commit. I tried to
implement the behavior as described here:
#1863 (comment)
  • Loading branch information
Holzhaus committed Oct 2, 2022
1 parent 68faf98 commit 83825f1
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 14 deletions.
5 changes: 2 additions & 3 deletions CONTRIBUTING.md
Expand Up @@ -11,7 +11,6 @@
- ruby + gem
- docker
- conda
- cargo (required by tests for rust dependencies)
- go (required by tests for go dependencies)
- swift

Expand Down Expand Up @@ -65,9 +64,9 @@ to implement. The current implemented languages are at varying levels:
- 0th class - pre-commit does not require any dependencies for these languages
as they're not actually languages (current examples: fail, pygrep)
- 1st class - pre-commit will bootstrap a full interpreter requiring nothing to
be installed globally (current examples: node, ruby)
be installed globally (current examples: node, ruby, rust)
- 2nd class - pre-commit requires the user to install the language globally but
will install tools in an isolated fashion (current examples: python, go, rust,
will install tools in an isolated fashion (current examples: python, go,
swift, docker).
- 3rd class - pre-commit requires the user to install both the tool and the
language globally (current examples: script, system)
Expand Down
101 changes: 90 additions & 11 deletions pre_commit/languages/rust.py
@@ -1,12 +1,19 @@
from __future__ import annotations

import contextlib
import functools
import os.path
import platform
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
Expand All @@ -15,19 +22,39 @@
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
TOOLCHAIN_FROM_VERSION = {
'system': None,
C.DEFAULT: 'stable',
}


@functools.lru_cache(maxsize=1)
def get_default_version() -> str:
# if ust is already installed, we can save a bunch of setup time by
# using the installed version
if helpers.exe_exists('cargo'):
return 'system'
else:
return C.DEFAULT


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) -> PatchesT:
def get_env_patch(target_dir: str, version: str) -> PatchesT:
toolchain = TOOLCHAIN_FROM_VERSION.get(version, version)
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', toolchain),) if toolchain else ()),
)


Expand All @@ -36,7 +63,9 @@ def in_env(
prefix: Prefix,
language_version: str,
) -> Generator[None, None, None]:
with envcontext(get_env_patch(_envdir(prefix, language_version))):
with envcontext(
get_env_patch(_envdir(prefix, language_version), language_version),
):
yield


Expand Down Expand Up @@ -64,13 +93,57 @@ def _add_dependencies(
f.truncate()


def install_rust_with_toolchain(toolchain: str) -> None:
# Check if rustup is already present in PATH
rustup_exe = parse_shebang.find_executable('rustup')
with tempfile.TemporaryDirectory() as rustup_dir:
rustup_env = (
('RUSTUP_HOME', rustup_dir),
)
with envcontext(rustup_env):
if rustup_exe is None:
# We did not detect rustup and need to download it first.
if sys.platform == 'win32':
if platform.machine() == 'x86_64':
url = 'https://win.rustup.rs/x86_64'
else:
url = 'https://win.rustup.rs/i686'
else:
url = 'https://sh.rustup.rs'

rustup_init_exe = os.path.join(
rustup_dir,
win_exe('rustup-init'),
)
urllib.request.urlretrieve(url, rustup_init_exe)
make_executable(rustup_init_exe)
_, _, _ = cmd_output_b(
rustup_init_exe, '-y', '--quiet', '--no-modify-path',
'--default-toolchain', 'none',
retcode=0,
)

# Now, rustup should be present in `$CARGO_HOME/bin`.
rustup_exe = parse_shebang.find_executable('rustup')

if rustup_exe is None:
raise AssertionError(
'failed to find rustup even though it should have been '
'installed at this point -- this is probably a bug',
)

_, _, _ = cmd_output_b(
rustup_exe, 'toolchain', 'install', '--no-self-update',
toolchain, retcode=0,
)


def install_environment(
prefix: Prefix,
version: str,
language_version: str,
additional_dependencies: Sequence[str],
) -> None:
helpers.assert_version_default('rust', version)
directory = _envdir(prefix, version)
directory = _envdir(prefix, language_version)

# There are two cases where we might want to specify more dependencies:
# as dependencies for the library being built, and as binary packages
Expand Down Expand Up @@ -100,11 +173,17 @@ def install_environment(
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, language_version):
toolchain = os.getenv('RUSTUP_TOOLCHAIN')
if toolchain is not None:
install_rust_with_toolchain(toolchain)

for args in packages_to_install:
cmd_output_b(
'cargo', 'install', '--bins', '--root', directory,
*args,
cwd=prefix.prefix_dir,
)


def run_hook(
Expand Down

0 comments on commit 83825f1

Please sign in to comment.