summaryrefslogtreecommitdiffstats
path: root/pre_commit/languages
diff options
context:
space:
mode:
Diffstat (limited to 'pre_commit/languages')
-rw-r--r--pre_commit/languages/dotnet.py67
-rw-r--r--pre_commit/languages/node.py2
-rw-r--r--pre_commit/languages/r.py5
-rw-r--r--pre_commit/languages/rust.py140
4 files changed, 163 insertions, 51 deletions
diff --git a/pre_commit/languages/dotnet.py b/pre_commit/languages/dotnet.py
index 3983c6f..e26b45c 100644
--- a/pre_commit/languages/dotnet.py
+++ b/pre_commit/languages/dotnet.py
@@ -2,6 +2,10 @@ from __future__ import annotations
import contextlib
import os.path
+import re
+import tempfile
+import xml.etree.ElementTree
+import zipfile
from typing import Generator
from typing import Sequence
@@ -35,6 +39,22 @@ def in_env(prefix: Prefix) -> Generator[None, None, None]:
yield
+@contextlib.contextmanager
+def _nuget_config_no_sources() -> Generator[str, None, None]:
+ with tempfile.TemporaryDirectory() as tmpdir:
+ nuget_config = os.path.join(tmpdir, 'nuget.config')
+ with open(nuget_config, 'w') as f:
+ f.write(
+ '<?xml version="1.0" encoding="utf-8"?>'
+ '<configuration>'
+ ' <packageSources>'
+ ' <clear />'
+ ' </packageSources>'
+ '</configuration>',
+ )
+ yield nuget_config
+
+
def install_environment(
prefix: Prefix,
version: str,
@@ -57,21 +77,42 @@ def install_environment(
),
)
- # Determine tool from the packaged file <tool_name>.<version>.nupkg
- build_outputs = os.listdir(os.path.join(prefix.prefix_dir, build_dir))
- for output in build_outputs:
- tool_name = output.split('.')[0]
+ nupkg_dir = prefix.path(build_dir)
+ nupkgs = [x for x in os.listdir(nupkg_dir) if x.endswith('.nupkg')]
+
+ if not nupkgs:
+ raise AssertionError('could not find any build outputs to install')
+
+ for nupkg in nupkgs:
+ with zipfile.ZipFile(os.path.join(nupkg_dir, nupkg)) as f:
+ nuspec, = (x for x in f.namelist() if x.endswith('.nuspec'))
+ with f.open(nuspec) as spec:
+ tree = xml.etree.ElementTree.parse(spec)
+
+ namespace = re.match(r'{.*}', tree.getroot().tag)
+ if not namespace:
+ raise AssertionError('could not parse namespace from nuspec')
+
+ tool_id_element = tree.find(f'.//{namespace[0]}id')
+ if tool_id_element is None:
+ raise AssertionError('expected to find an "id" element')
+
+ tool_id = tool_id_element.text
+ if not tool_id:
+ raise AssertionError('"id" element missing tool name')
# Install to bin dir
- helpers.run_setup_cmd(
- prefix,
- (
- 'dotnet', 'tool', 'install',
- '--tool-path', os.path.join(envdir, BIN_DIR),
- '--add-source', build_dir,
- tool_name,
- ),
- )
+ with _nuget_config_no_sources() as nuget_config:
+ helpers.run_setup_cmd(
+ prefix,
+ (
+ 'dotnet', 'tool', 'install',
+ '--configfile', nuget_config,
+ '--tool-path', os.path.join(envdir, BIN_DIR),
+ '--add-source', build_dir,
+ tool_id,
+ ),
+ )
# Clean the git dir, ignoring the environment dir
clean_cmd = ('git', 'clean', '-ffxd', '-e', f'{ENVIRONMENT_DIR}-*')
diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py
index 39f3000..37a5b63 100644
--- a/pre_commit/languages/node.py
+++ b/pre_commit/languages/node.py
@@ -75,7 +75,7 @@ def in_env(
def health_check(prefix: Prefix, language_version: str) -> str | None:
with in_env(prefix, language_version):
- retcode, _, _ = cmd_output_b('node', '--version', retcode=None)
+ retcode, _, _ = cmd_output_b('node', '--version', check=False)
if retcode != 0: # pragma: win32 no cover
return f'`node --version` returned {retcode}'
else:
diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py
index 40a001d..d281102 100644
--- a/pre_commit/languages/r.py
+++ b/pre_commit/languages/r.py
@@ -15,6 +15,7 @@ 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 win_exe
ENVIRONMENT_DIR = 'renv'
RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ')
@@ -63,7 +64,7 @@ def _rscript_exec() -> str:
if r_home is None:
return 'Rscript'
else:
- return os.path.join(r_home, 'bin', 'Rscript')
+ return os.path.join(r_home, 'bin', win_exe('Rscript'))
def _entry_validate(entry: Sequence[str]) -> None:
@@ -158,7 +159,7 @@ def _inline_r_setup(code: str) -> str:
only be configured via R options once R has started. These are set here.
"""
with_option = f"""\
- options(install.packages.compile.from.source = "never")
+ options(install.packages.compile.from.source = "never", pkgType = "binary")
{code}
"""
return with_option
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)