summaryrefslogtreecommitdiffstats
path: root/pre_commit/languages
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2020-03-24 21:59:15 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2020-03-24 21:59:15 +0000
commit63fad53303381388673073de580a32088a4ef0fe (patch)
treea2c5c329ee5e79a220fac7e079283235fecc0cda /pre_commit/languages
parentInitial commit. (diff)
downloadpre-commit-63fad53303381388673073de580a32088a4ef0fe.tar.xz
pre-commit-63fad53303381388673073de580a32088a4ef0fe.zip
Adding upstream version 2.2.0.upstream/2.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'pre_commit/languages')
-rw-r--r--pre_commit/languages/__init__.py0
-rw-r--r--pre_commit/languages/all.py60
-rw-r--r--pre_commit/languages/conda.py84
-rw-r--r--pre_commit/languages/docker.py114
-rw-r--r--pre_commit/languages/docker_image.py22
-rw-r--r--pre_commit/languages/fail.py20
-rw-r--r--pre_commit/languages/golang.py97
-rw-r--r--pre_commit/languages/helpers.py109
-rw-r--r--pre_commit/languages/node.py93
-rw-r--r--pre_commit/languages/perl.py67
-rw-r--r--pre_commit/languages/pygrep.py87
-rw-r--r--pre_commit/languages/python.py210
-rw-r--r--pre_commit/languages/python_venv.py46
-rw-r--r--pre_commit/languages/ruby.py126
-rw-r--r--pre_commit/languages/rust.py106
-rw-r--r--pre_commit/languages/script.py19
-rw-r--r--pre_commit/languages/swift.py64
-rw-r--r--pre_commit/languages/system.py19
18 files changed, 1343 insertions, 0 deletions
diff --git a/pre_commit/languages/__init__.py b/pre_commit/languages/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pre_commit/languages/__init__.py
diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py
new file mode 100644
index 0000000..8f4ffa8
--- /dev/null
+++ b/pre_commit/languages/all.py
@@ -0,0 +1,60 @@
+from typing import Callable
+from typing import NamedTuple
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+
+from pre_commit.hook import Hook
+from pre_commit.languages import conda
+from pre_commit.languages import docker
+from pre_commit.languages import docker_image
+from pre_commit.languages import fail
+from pre_commit.languages import golang
+from pre_commit.languages import node
+from pre_commit.languages import perl
+from pre_commit.languages import pygrep
+from pre_commit.languages import python
+from pre_commit.languages import python_venv
+from pre_commit.languages import ruby
+from pre_commit.languages import rust
+from pre_commit.languages import script
+from pre_commit.languages import swift
+from pre_commit.languages import system
+from pre_commit.prefix import Prefix
+
+
+class Language(NamedTuple):
+ name: str
+ # Use `None` for no installation / environment
+ ENVIRONMENT_DIR: Optional[str]
+ # return a value to replace `'default` for `language_version`
+ get_default_version: Callable[[], str]
+ # return whether the environment is healthy (or should be rebuilt)
+ healthy: Callable[[Prefix, str], bool]
+ # install a repository for the given language and language_version
+ install_environment: Callable[[Prefix, str, Sequence[str]], None]
+ # execute a hook and return the exit code and output
+ run_hook: 'Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]]'
+
+
+# TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018
+languages = {
+ # BEGIN GENERATED (testing/gen-languages-all)
+ 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501
+ 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501
+ 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501
+ 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501
+ 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501
+ 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501
+ 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501
+ 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501
+ 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501
+ 'python_venv': Language(name='python_venv', ENVIRONMENT_DIR=python_venv.ENVIRONMENT_DIR, get_default_version=python_venv.get_default_version, healthy=python_venv.healthy, install_environment=python_venv.install_environment, run_hook=python_venv.run_hook), # noqa: E501
+ 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501
+ 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501
+ 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501
+ 'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, healthy=swift.healthy, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501
+ 'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501
+ # END GENERATED
+}
+all_languages = sorted(languages)
diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py
new file mode 100644
index 0000000..071757a
--- /dev/null
+++ b/pre_commit/languages/conda.py
@@ -0,0 +1,84 @@
+import contextlib
+import os
+from typing import Generator
+from typing import Sequence
+from typing import Tuple
+
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import PatchesT
+from pre_commit.envcontext import SubstitutionT
+from pre_commit.envcontext import UNSET
+from pre_commit.envcontext import Var
+from pre_commit.hook import Hook
+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
+
+ENVIRONMENT_DIR = 'conda'
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+
+
+def get_env_patch(env: str) -> PatchesT:
+ # On non-windows systems executable live in $CONDA_PREFIX/bin, on Windows
+ # they can be in $CONDA_PREFIX/bin, $CONDA_PREFIX/Library/bin,
+ # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only
+ # seems to be used for python.exe.
+ path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))
+ if os.name == 'nt': # pragma: no cover (platform specific)
+ path = (env, os.pathsep, *path)
+ path = (os.path.join(env, 'Scripts'), os.pathsep, *path)
+ path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path)
+
+ return (
+ ('PYTHONHOME', UNSET),
+ ('VIRTUAL_ENV', UNSET),
+ ('CONDA_PREFIX', env),
+ ('PATH', path),
+ )
+
+
+@contextlib.contextmanager
+def in_env(
+ prefix: Prefix,
+ language_version: str,
+) -> Generator[None, None, None]:
+ directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
+ envdir = prefix.path(directory)
+ with envcontext(get_env_patch(envdir)):
+ yield
+
+
+def install_environment(
+ prefix: Prefix,
+ version: str,
+ additional_dependencies: Sequence[str],
+) -> None:
+ helpers.assert_version_default('conda', version)
+ directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
+
+ env_dir = prefix.path(directory)
+ with clean_path_on_failure(env_dir):
+ cmd_output_b(
+ 'conda', 'env', 'create', '-p', env_dir, '--file',
+ 'environment.yml', cwd=prefix.prefix_dir,
+ )
+ if additional_dependencies:
+ cmd_output_b(
+ 'conda', 'install', '-p', env_dir, *additional_dependencies,
+ cwd=prefix.prefix_dir,
+ )
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]:
+ # TODO: Some rare commands need to be run using `conda run` but mostly we
+ # can run them withot which is much quicker and produces a better
+ # output.
+ # cmd = ('conda', 'run', '-p', env_dir) + hook.cmd
+ with in_env(hook.prefix, hook.language_version):
+ return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py
new file mode 100644
index 0000000..f449584
--- /dev/null
+++ b/pre_commit/languages/docker.py
@@ -0,0 +1,114 @@
+import hashlib
+import os
+from typing import Sequence
+from typing import Tuple
+
+import pre_commit.constants as C
+from pre_commit.hook import Hook
+from pre_commit.languages import helpers
+from pre_commit.prefix import Prefix
+from pre_commit.util import CalledProcessError
+from pre_commit.util import clean_path_on_failure
+from pre_commit.util import cmd_output_b
+
+ENVIRONMENT_DIR = 'docker'
+PRE_COMMIT_LABEL = 'PRE_COMMIT'
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+
+
+def md5(s: str) -> str: # pragma: win32 no cover
+ return hashlib.md5(s.encode()).hexdigest()
+
+
+def docker_tag(prefix: Prefix) -> str: # pragma: win32 no cover
+ md5sum = md5(os.path.basename(prefix.prefix_dir)).lower()
+ return f'pre-commit-{md5sum}'
+
+
+def docker_is_running() -> bool: # pragma: win32 no cover
+ try:
+ cmd_output_b('docker', 'ps')
+ except CalledProcessError:
+ return False
+ else:
+ return True
+
+
+def assert_docker_available() -> None: # pragma: win32 no cover
+ assert docker_is_running(), (
+ 'Docker is either not running or not configured in this environment'
+ )
+
+
+def build_docker_image(
+ prefix: Prefix,
+ *,
+ pull: bool,
+) -> None: # pragma: win32 no cover
+ cmd: Tuple[str, ...] = (
+ 'docker', 'build',
+ '--tag', docker_tag(prefix),
+ '--label', PRE_COMMIT_LABEL,
+ )
+ if pull:
+ cmd += ('--pull',)
+ # This must come last for old versions of docker. See #477
+ cmd += ('.',)
+ helpers.run_setup_cmd(prefix, cmd)
+
+
+def install_environment(
+ prefix: Prefix, version: str, additional_dependencies: Sequence[str],
+) -> None: # pragma: win32 no cover
+ helpers.assert_version_default('docker', version)
+ helpers.assert_no_additional_deps('docker', additional_dependencies)
+ assert_docker_available()
+
+ directory = prefix.path(
+ helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
+ )
+
+ # Docker doesn't really have relevant disk environment, but pre-commit
+ # still needs to cleanup its state files on failure
+ with clean_path_on_failure(directory):
+ build_docker_image(prefix, pull=True)
+ os.mkdir(directory)
+
+
+def get_docker_user() -> str: # pragma: win32 no cover
+ try:
+ return f'{os.getuid()}:{os.getgid()}'
+ except AttributeError:
+ return '1000:1000'
+
+
+def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover
+ return (
+ 'docker', 'run',
+ '--rm',
+ '-u', get_docker_user(),
+ # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from
+ # The `Z` option tells Docker to label the content with a private
+ # unshared label. Only the current container can use a private volume.
+ '-v', f'{os.getcwd()}:/src:rw,Z',
+ '--workdir', '/src',
+ )
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]: # pragma: win32 no cover
+ assert_docker_available()
+ # Rebuild the docker image in case it has gone missing, as many people do
+ # automated cleanup of docker images.
+ build_docker_image(hook.prefix, pull=False)
+
+ hook_cmd = hook.cmd
+ entry_exe, cmd_rest = hook.cmd[0], hook_cmd[1:]
+
+ entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix))
+ cmd = docker_cmd() + entry_tag + cmd_rest
+ return helpers.run_xargs(hook, cmd, file_args, color=color)
diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py
new file mode 100644
index 0000000..0c51df6
--- /dev/null
+++ b/pre_commit/languages/docker_image.py
@@ -0,0 +1,22 @@
+from typing import Sequence
+from typing import Tuple
+
+from pre_commit.hook import Hook
+from pre_commit.languages import helpers
+from pre_commit.languages.docker import assert_docker_available
+from pre_commit.languages.docker import docker_cmd
+
+ENVIRONMENT_DIR = None
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+install_environment = helpers.no_install
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]: # pragma: win32 no cover
+ assert_docker_available()
+ cmd = docker_cmd() + hook.cmd
+ return helpers.run_xargs(hook, cmd, file_args, color=color)
diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py
new file mode 100644
index 0000000..d2b02d2
--- /dev/null
+++ b/pre_commit/languages/fail.py
@@ -0,0 +1,20 @@
+from typing import Sequence
+from typing import Tuple
+
+from pre_commit.hook import Hook
+from pre_commit.languages import helpers
+
+ENVIRONMENT_DIR = None
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+install_environment = helpers.no_install
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]:
+ out = f'{hook.entry}\n\n'.encode()
+ out += b'\n'.join(f.encode() for f in file_args) + b'\n'
+ return 1, out
diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py
new file mode 100644
index 0000000..91ade1e
--- /dev/null
+++ b/pre_commit/languages/golang.py
@@ -0,0 +1,97 @@
+import contextlib
+import os.path
+import sys
+from typing import Generator
+from typing import Sequence
+from typing import Tuple
+
+import pre_commit.constants as C
+from pre_commit import git
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import PatchesT
+from pre_commit.envcontext import Var
+from pre_commit.hook import Hook
+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
+from pre_commit.util import cmd_output_b
+from pre_commit.util import rmtree
+
+ENVIRONMENT_DIR = 'golangenv'
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+
+
+def get_env_patch(venv: str) -> PatchesT:
+ return (
+ ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
+ )
+
+
+@contextlib.contextmanager
+def in_env(prefix: Prefix) -> Generator[None, None, None]:
+ envdir = prefix.path(
+ helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
+ )
+ with envcontext(get_env_patch(envdir)):
+ yield
+
+
+def guess_go_dir(remote_url: str) -> str:
+ if remote_url.endswith('.git'):
+ remote_url = remote_url[:-1 * len('.git')]
+ looks_like_url = (
+ not remote_url.startswith('file://') and
+ ('//' in remote_url or '@' in remote_url)
+ )
+ remote_url = remote_url.replace(':', '/')
+ if looks_like_url:
+ _, _, remote_url = remote_url.rpartition('//')
+ _, _, remote_url = remote_url.rpartition('@')
+ return remote_url
+ else:
+ return 'unknown_src_dir'
+
+
+def install_environment(
+ prefix: Prefix,
+ version: str,
+ additional_dependencies: Sequence[str],
+) -> None:
+ helpers.assert_version_default('golang', version)
+ directory = prefix.path(
+ helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
+ )
+
+ with clean_path_on_failure(directory):
+ remote = git.get_remote_url(prefix.prefix_dir)
+ repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote))
+
+ # Clone into the goenv we'll create
+ helpers.run_setup_cmd(prefix, ('git', 'clone', '.', repo_src_dir))
+
+ if sys.platform == 'cygwin': # pragma: no cover
+ _, gopath, _ = cmd_output('cygpath', '-w', directory)
+ gopath = gopath.strip()
+ else:
+ gopath = directory
+ env = dict(os.environ, GOPATH=gopath)
+ env.pop('GOBIN', None)
+ cmd_output_b('go', 'get', './...', cwd=repo_src_dir, env=env)
+ for dependency in additional_dependencies:
+ cmd_output_b('go', 'get', dependency, cwd=repo_src_dir, env=env)
+ # Same some disk space, we don't need these after installation
+ rmtree(prefix.path(directory, 'src'))
+ pkgdir = prefix.path(directory, 'pkg')
+ if os.path.exists(pkgdir): # pragma: no cover (go<1.10)
+ rmtree(pkgdir)
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]:
+ with in_env(hook.prefix):
+ return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py
new file mode 100644
index 0000000..b5c95e5
--- /dev/null
+++ b/pre_commit/languages/helpers.py
@@ -0,0 +1,109 @@
+import multiprocessing
+import os
+import random
+from typing import Any
+from typing import List
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import Tuple
+from typing import TYPE_CHECKING
+
+import pre_commit.constants as C
+from pre_commit.hook import Hook
+from pre_commit.prefix import Prefix
+from pre_commit.util import cmd_output_b
+from pre_commit.xargs import xargs
+
+if TYPE_CHECKING:
+ from typing import NoReturn
+
+FIXED_RANDOM_SEED = 1542676186
+
+
+def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None:
+ cmd_output_b(*cmd, cwd=prefix.prefix_dir)
+
+
+@overload
+def environment_dir(d: None, language_version: str) -> None: ...
+@overload
+def environment_dir(d: str, language_version: str) -> str: ...
+
+
+def environment_dir(d: Optional[str], language_version: str) -> Optional[str]:
+ if d is None:
+ return None
+ else:
+ return f'{d}-{language_version}'
+
+
+def assert_version_default(binary: str, version: str) -> None:
+ if version != C.DEFAULT:
+ raise AssertionError(
+ f'For now, pre-commit requires system-installed {binary}',
+ )
+
+
+def assert_no_additional_deps(
+ lang: str,
+ additional_deps: Sequence[str],
+) -> None:
+ if additional_deps:
+ raise AssertionError(
+ f'For now, pre-commit does not support '
+ f'additional_dependencies for {lang}',
+ )
+
+
+def basic_get_default_version() -> str:
+ return C.DEFAULT
+
+
+def basic_healthy(prefix: Prefix, language_version: str) -> bool:
+ return True
+
+
+def no_install(
+ prefix: Prefix,
+ version: str,
+ additional_dependencies: Sequence[str],
+) -> 'NoReturn':
+ raise AssertionError('This type is not installable')
+
+
+def target_concurrency(hook: Hook) -> int:
+ if hook.require_serial or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ:
+ return 1
+ else:
+ # Travis appears to have a bunch of CPUs, but we can't use them all.
+ if 'TRAVIS' in os.environ:
+ return 2
+ else:
+ try:
+ return multiprocessing.cpu_count()
+ except NotImplementedError:
+ return 1
+
+
+def _shuffled(seq: Sequence[str]) -> List[str]:
+ """Deterministically shuffle"""
+ fixed_random = random.Random()
+ fixed_random.seed(FIXED_RANDOM_SEED, version=1)
+
+ seq = list(seq)
+ random.shuffle(seq, random=fixed_random.random)
+ return seq
+
+
+def run_xargs(
+ hook: Hook,
+ cmd: Tuple[str, ...],
+ file_args: Sequence[str],
+ **kwargs: Any,
+) -> Tuple[int, bytes]:
+ # Shuffle the files so that they more evenly fill out the xargs partitions,
+ # but do it deterministically in case a hook cares about ordering.
+ file_args = _shuffled(file_args)
+ kwargs['target_concurrency'] = target_concurrency(hook)
+ return xargs(cmd, file_args, **kwargs)
diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py
new file mode 100644
index 0000000..79ff807
--- /dev/null
+++ b/pre_commit/languages/node.py
@@ -0,0 +1,93 @@
+import contextlib
+import os
+import sys
+from typing import Generator
+from typing import Sequence
+from typing import Tuple
+
+import pre_commit.constants as C
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import PatchesT
+from pre_commit.envcontext import Var
+from pre_commit.hook import Hook
+from pre_commit.languages import helpers
+from pre_commit.languages.python import bin_dir
+from pre_commit.prefix import Prefix
+from pre_commit.util import clean_path_on_failure
+from pre_commit.util import cmd_output
+from pre_commit.util import cmd_output_b
+
+ENVIRONMENT_DIR = 'node_env'
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+
+
+def _envdir(prefix: Prefix, version: str) -> str:
+ directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
+ return prefix.path(directory)
+
+
+def get_env_patch(venv: str) -> PatchesT:
+ if sys.platform == 'cygwin': # pragma: no cover
+ _, win_venv, _ = cmd_output('cygpath', '-w', venv)
+ install_prefix = fr'{win_venv.strip()}\bin'
+ lib_dir = 'lib'
+ elif sys.platform == 'win32': # pragma: no cover
+ install_prefix = bin_dir(venv)
+ lib_dir = 'Scripts'
+ else: # pragma: win32 no cover
+ install_prefix = venv
+ lib_dir = 'lib'
+ return (
+ ('NODE_VIRTUAL_ENV', venv),
+ ('NPM_CONFIG_PREFIX', install_prefix),
+ ('npm_config_prefix', install_prefix),
+ ('NODE_PATH', os.path.join(venv, lib_dir, 'node_modules')),
+ ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))),
+ )
+
+
+@contextlib.contextmanager
+def in_env(
+ prefix: Prefix,
+ language_version: str,
+) -> Generator[None, None, None]:
+ with envcontext(get_env_patch(_envdir(prefix, language_version))):
+ yield
+
+
+def install_environment(
+ prefix: Prefix, version: str, additional_dependencies: Sequence[str],
+) -> None:
+ additional_dependencies = tuple(additional_dependencies)
+ assert prefix.exists('package.json')
+ envdir = _envdir(prefix, version)
+
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath
+ if sys.platform == 'win32': # pragma: no cover
+ envdir = f'\\\\?\\{os.path.normpath(envdir)}'
+ with clean_path_on_failure(envdir):
+ cmd = [
+ sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir,
+ ]
+ if version != C.DEFAULT:
+ cmd.extend(['-n', version])
+ cmd_output_b(*cmd)
+
+ with in_env(prefix, version):
+ # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449
+ # install as if we installed from git
+ helpers.run_setup_cmd(prefix, ('npm', 'install'))
+ helpers.run_setup_cmd(
+ prefix,
+ ('npm', 'install', '-g', '.', *additional_dependencies),
+ )
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]:
+ with in_env(hook.prefix, hook.language_version):
+ return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py
new file mode 100644
index 0000000..bbf5504
--- /dev/null
+++ b/pre_commit/languages/perl.py
@@ -0,0 +1,67 @@
+import contextlib
+import os
+import shlex
+from typing import Generator
+from typing import Sequence
+from typing import Tuple
+
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import PatchesT
+from pre_commit.envcontext import Var
+from pre_commit.hook import Hook
+from pre_commit.languages import helpers
+from pre_commit.prefix import Prefix
+from pre_commit.util import clean_path_on_failure
+
+ENVIRONMENT_DIR = 'perl_env'
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+
+
+def _envdir(prefix: Prefix, version: str) -> str:
+ directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
+ return prefix.path(directory)
+
+
+def get_env_patch(venv: str) -> PatchesT:
+ return (
+ ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
+ ('PERL5LIB', os.path.join(venv, 'lib', 'perl5')),
+ ('PERL_MB_OPT', f'--install_base {shlex.quote(venv)}'),
+ (
+ 'PERL_MM_OPT', (
+ f'INSTALL_BASE={shlex.quote(venv)} '
+ f'INSTALLSITEMAN1DIR=none INSTALLSITEMAN3DIR=none'
+ ),
+ ),
+ )
+
+
+@contextlib.contextmanager
+def in_env(
+ prefix: Prefix,
+ language_version: str,
+) -> Generator[None, None, None]:
+ with envcontext(get_env_patch(_envdir(prefix, language_version))):
+ yield
+
+
+def install_environment(
+ prefix: Prefix, version: str, additional_dependencies: Sequence[str],
+) -> None:
+ helpers.assert_version_default('perl', version)
+
+ with clean_path_on_failure(_envdir(prefix, version)):
+ with in_env(prefix, version):
+ helpers.run_setup_cmd(
+ prefix, ('cpan', '-T', '.', *additional_dependencies),
+ )
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]:
+ with in_env(hook.prefix, hook.language_version):
+ return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py
new file mode 100644
index 0000000..40adba0
--- /dev/null
+++ b/pre_commit/languages/pygrep.py
@@ -0,0 +1,87 @@
+import argparse
+import re
+import sys
+from typing import Optional
+from typing import Pattern
+from typing import Sequence
+from typing import Tuple
+
+from pre_commit import output
+from pre_commit.hook import Hook
+from pre_commit.languages import helpers
+from pre_commit.xargs import xargs
+
+ENVIRONMENT_DIR = None
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+install_environment = helpers.no_install
+
+
+def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int:
+ retv = 0
+ with open(filename, 'rb') as f:
+ for line_no, line in enumerate(f, start=1):
+ if pattern.search(line):
+ retv = 1
+ output.write(f'{filename}:{line_no}:')
+ output.write_line_b(line.rstrip(b'\r\n'))
+ return retv
+
+
+def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int:
+ retv = 0
+ with open(filename, 'rb') as f:
+ contents = f.read()
+ match = pattern.search(contents)
+ if match:
+ retv = 1
+ line_no = contents[:match.start()].count(b'\n')
+ output.write(f'{filename}:{line_no + 1}:')
+
+ matched_lines = match[0].split(b'\n')
+ matched_lines[0] = contents.split(b'\n')[line_no]
+
+ output.write_line_b(b'\n'.join(matched_lines))
+ return retv
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]:
+ exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,)
+ return xargs(exe, file_args, color=color)
+
+
+def main(argv: Optional[Sequence[str]] = None) -> int:
+ parser = argparse.ArgumentParser(
+ description=(
+ 'grep-like finder using python regexes. Unlike grep, this tool '
+ 'returns nonzero when it finds a match and zero otherwise. The '
+ 'idea here being that matches are "problems".'
+ ),
+ )
+ parser.add_argument('-i', '--ignore-case', action='store_true')
+ parser.add_argument('--multiline', action='store_true')
+ parser.add_argument('pattern', help='python regex pattern.')
+ parser.add_argument('filenames', nargs='*')
+ args = parser.parse_args(argv)
+
+ flags = re.IGNORECASE if args.ignore_case else 0
+ if args.multiline:
+ flags |= re.MULTILINE | re.DOTALL
+
+ pattern = re.compile(args.pattern.encode(), flags)
+
+ retv = 0
+ for filename in args.filenames:
+ if args.multiline:
+ retv |= _process_filename_at_once(pattern, filename)
+ else:
+ retv |= _process_filename_by_line(pattern, filename)
+ return retv
+
+
+if __name__ == '__main__':
+ exit(main())
diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py
new file mode 100644
index 0000000..5073a8b
--- /dev/null
+++ b/pre_commit/languages/python.py
@@ -0,0 +1,210 @@
+import contextlib
+import functools
+import os
+import sys
+from typing import Callable
+from typing import ContextManager
+from typing import Generator
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+
+import pre_commit.constants as C
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import PatchesT
+from pre_commit.envcontext import UNSET
+from pre_commit.envcontext import Var
+from pre_commit.hook import Hook
+from pre_commit.languages import helpers
+from pre_commit.parse_shebang import find_executable
+from pre_commit.prefix import Prefix
+from pre_commit.util import CalledProcessError
+from pre_commit.util import clean_path_on_failure
+from pre_commit.util import cmd_output
+from pre_commit.util import cmd_output_b
+
+ENVIRONMENT_DIR = 'py_env'
+
+
+def bin_dir(venv: str) -> str:
+ """On windows there's a different directory for the virtualenv"""
+ bin_part = 'Scripts' if os.name == 'nt' else 'bin'
+ return os.path.join(venv, bin_part)
+
+
+def get_env_patch(venv: str) -> PatchesT:
+ return (
+ ('PYTHONHOME', UNSET),
+ ('VIRTUAL_ENV', venv),
+ ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))),
+ )
+
+
+def _find_by_py_launcher(
+ version: str,
+) -> Optional[str]: # pragma: no cover (windows only)
+ if version.startswith('python'):
+ num = version[len('python'):]
+ try:
+ cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)')
+ return cmd_output(*cmd)[1].strip()
+ except CalledProcessError:
+ pass
+ return None
+
+
+def _find_by_sys_executable() -> Optional[str]:
+ def _norm(path: str) -> Optional[str]:
+ _, exe = os.path.split(path.lower())
+ exe, _, _ = exe.partition('.exe')
+ if exe not in {'python', 'pythonw'} and find_executable(exe):
+ return exe
+ return None
+
+ # On linux, I see these common sys.executables:
+ #
+ # system `python`: /usr/bin/python -> python2.7
+ # system `python2`: /usr/bin/python2 -> python2.7
+ # virtualenv v: v/bin/python (will not return from this loop)
+ # virtualenv v -ppython2: v/bin/python -> python2
+ # virtualenv v -ppython2.7: v/bin/python -> python2.7
+ # virtualenv v -ppypy: v/bin/python -> v/bin/pypy
+ for path in (sys.executable, os.path.realpath(sys.executable)):
+ exe = _norm(path)
+ if exe:
+ return exe
+ return None
+
+
+@functools.lru_cache(maxsize=1)
+def get_default_version() -> str: # pragma: no cover (platform dependent)
+ # First attempt from `sys.executable` (or the realpath)
+ exe = _find_by_sys_executable()
+ if exe:
+ return exe
+
+ # Next try the `pythonX.X` executable
+ exe = f'python{sys.version_info[0]}.{sys.version_info[1]}'
+ if find_executable(exe):
+ return exe
+
+ if _find_by_py_launcher(exe):
+ return exe
+
+ # Give a best-effort try for windows
+ default_folder_name = exe.replace('.', '')
+ if os.path.exists(fr'C:\{default_folder_name}\python.exe'):
+ return exe
+
+ # We tried!
+ return C.DEFAULT
+
+
+def _sys_executable_matches(version: str) -> bool:
+ if version == 'python':
+ return True
+ elif not version.startswith('python'):
+ return False
+
+ try:
+ info = tuple(int(p) for p in version[len('python'):].split('.'))
+ except ValueError:
+ return False
+
+ return sys.version_info[:len(info)] == info
+
+
+def norm_version(version: str) -> str:
+ # first see if our current executable is appropriate
+ if _sys_executable_matches(version):
+ return sys.executable
+
+ if os.name == 'nt': # pragma: no cover (windows)
+ version_exec = _find_by_py_launcher(version)
+ if version_exec:
+ return version_exec
+
+ # Try looking up by name
+ version_exec = find_executable(version)
+ if version_exec and version_exec != version:
+ return version_exec
+
+ # If it is in the form pythonx.x search in the default
+ # place on windows
+ if version.startswith('python'):
+ default_folder_name = version.replace('.', '')
+ return fr'C:\{default_folder_name}\python.exe'
+
+ # Otherwise assume it is a path
+ return os.path.expanduser(version)
+
+
+def py_interface(
+ _dir: str,
+ _make_venv: Callable[[str, str], None],
+) -> Tuple[
+ Callable[[Prefix, str], ContextManager[None]],
+ Callable[[Prefix, str], bool],
+ Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]],
+ Callable[[Prefix, str, Sequence[str]], None],
+]:
+ @contextlib.contextmanager
+ def in_env(
+ prefix: Prefix,
+ language_version: str,
+ ) -> Generator[None, None, None]:
+ envdir = prefix.path(helpers.environment_dir(_dir, language_version))
+ with envcontext(get_env_patch(envdir)):
+ yield
+
+ def healthy(prefix: Prefix, language_version: str) -> bool:
+ envdir = helpers.environment_dir(_dir, language_version)
+ exe_name = 'python.exe' if sys.platform == 'win32' else 'python'
+ py_exe = prefix.path(bin_dir(envdir), exe_name)
+ with in_env(prefix, language_version):
+ retcode, _, _ = cmd_output_b(
+ py_exe, '-c', 'import ctypes, datetime, io, os, ssl, weakref',
+ cwd='/',
+ retcode=None,
+ )
+ return retcode == 0
+
+ def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+ ) -> Tuple[int, bytes]:
+ with in_env(hook.prefix, hook.language_version):
+ return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
+
+ def install_environment(
+ prefix: Prefix,
+ version: str,
+ additional_dependencies: Sequence[str],
+ ) -> None:
+ additional_dependencies = tuple(additional_dependencies)
+ directory = helpers.environment_dir(_dir, version)
+
+ env_dir = prefix.path(directory)
+ with clean_path_on_failure(env_dir):
+ if version != C.DEFAULT:
+ python = norm_version(version)
+ else:
+ python = os.path.realpath(sys.executable)
+ _make_venv(env_dir, python)
+ with in_env(prefix, version):
+ helpers.run_setup_cmd(
+ prefix, ('pip', 'install', '.') + additional_dependencies,
+ )
+
+ return in_env, healthy, run_hook, install_environment
+
+
+def make_venv(envdir: str, python: str) -> None:
+ env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1')
+ cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python)
+ cmd_output_b(*cmd, env=env, cwd='/')
+
+
+_interface = py_interface(ENVIRONMENT_DIR, make_venv)
+in_env, healthy, run_hook, install_environment = _interface
diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py
new file mode 100644
index 0000000..5404c8b
--- /dev/null
+++ b/pre_commit/languages/python_venv.py
@@ -0,0 +1,46 @@
+import os.path
+
+from pre_commit.languages import python
+from pre_commit.util import CalledProcessError
+from pre_commit.util import cmd_output
+from pre_commit.util import cmd_output_b
+
+ENVIRONMENT_DIR = 'py_venv'
+get_default_version = python.get_default_version
+
+
+def orig_py_exe(exe: str) -> str: # pragma: no cover (platform specific)
+ """A -mvenv virtualenv made from a -mvirtualenv virtualenv installs
+ packages to the incorrect location. Attempt to find the _original_ exe
+ and invoke `-mvenv` from there.
+
+ See:
+ - https://github.com/pre-commit/pre-commit/issues/755
+ - https://github.com/pypa/virtualenv/issues/1095
+ - https://bugs.python.org/issue30811
+ """
+ try:
+ prefix_script = 'import sys; print(sys.real_prefix)'
+ _, prefix, _ = cmd_output(exe, '-c', prefix_script)
+ prefix = prefix.strip()
+ except CalledProcessError:
+ # not created from -mvirtualenv
+ return exe
+
+ if os.name == 'nt':
+ expected = os.path.join(prefix, 'python.exe')
+ else:
+ expected = os.path.join(prefix, 'bin', os.path.basename(exe))
+
+ if os.path.exists(expected):
+ return expected
+ else:
+ return exe
+
+
+def make_venv(envdir: str, python: str) -> None:
+ cmd_output_b(orig_py_exe(python), '-mvenv', envdir, cwd='/')
+
+
+_interface = python.py_interface(ENVIRONMENT_DIR, make_venv)
+in_env, healthy, run_hook, install_environment = _interface
diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py
new file mode 100644
index 0000000..61241f8
--- /dev/null
+++ b/pre_commit/languages/ruby.py
@@ -0,0 +1,126 @@
+import contextlib
+import os.path
+import shutil
+import tarfile
+from typing import Generator
+from typing import Sequence
+from typing import Tuple
+
+import pre_commit.constants as C
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import PatchesT
+from pre_commit.envcontext import Var
+from pre_commit.hook import Hook
+from pre_commit.languages import helpers
+from pre_commit.prefix import Prefix
+from pre_commit.util import CalledProcessError
+from pre_commit.util import clean_path_on_failure
+from pre_commit.util import resource_bytesio
+
+ENVIRONMENT_DIR = 'rbenv'
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+
+
+def get_env_patch(
+ venv: str,
+ language_version: str,
+) -> PatchesT: # pragma: win32 no cover
+ patches: PatchesT = (
+ ('GEM_HOME', os.path.join(venv, 'gems')),
+ ('RBENV_ROOT', venv),
+ ('BUNDLE_IGNORE_CONFIG', '1'),
+ (
+ 'PATH', (
+ os.path.join(venv, 'gems', 'bin'), os.pathsep,
+ os.path.join(venv, 'shims'), os.pathsep,
+ os.path.join(venv, 'bin'), os.pathsep, Var('PATH'),
+ ),
+ ),
+ )
+ if language_version != C.DEFAULT:
+ patches += (('RBENV_VERSION', language_version),)
+ return patches
+
+
+@contextlib.contextmanager # pragma: win32 no cover
+def in_env(
+ prefix: Prefix,
+ language_version: str,
+) -> Generator[None, None, None]:
+ envdir = prefix.path(
+ helpers.environment_dir(ENVIRONMENT_DIR, language_version),
+ )
+ with envcontext(get_env_patch(envdir, language_version)):
+ yield
+
+
+def _extract_resource(filename: str, dest: str) -> None:
+ with resource_bytesio(filename) as bio:
+ with tarfile.open(fileobj=bio) as tf:
+ tf.extractall(dest)
+
+
+def _install_rbenv(
+ prefix: Prefix,
+ version: str = C.DEFAULT,
+) -> None: # pragma: win32 no cover
+ directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
+
+ _extract_resource('rbenv.tar.gz', prefix.path('.'))
+ shutil.move(prefix.path('rbenv'), prefix.path(directory))
+
+ # Only install ruby-build if the version is specified
+ if version != C.DEFAULT:
+ plugins_dir = prefix.path(directory, 'plugins')
+ _extract_resource('ruby-download.tar.gz', plugins_dir)
+ _extract_resource('ruby-build.tar.gz', plugins_dir)
+
+
+def _install_ruby(
+ prefix: Prefix,
+ version: str,
+) -> None: # pragma: win32 no cover
+ try:
+ helpers.run_setup_cmd(prefix, ('rbenv', 'download', version))
+ except CalledProcessError: # pragma: no cover (usually find with download)
+ # Failed to download from mirror for some reason, build it instead
+ helpers.run_setup_cmd(prefix, ('rbenv', 'install', version))
+
+
+def install_environment(
+ prefix: Prefix, version: str, additional_dependencies: Sequence[str],
+) -> None: # pragma: win32 no cover
+ additional_dependencies = tuple(additional_dependencies)
+ directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
+ with clean_path_on_failure(prefix.path(directory)):
+ # TODO: this currently will fail if there's no version specified and
+ # there's no system ruby installed. Is this ok?
+ _install_rbenv(prefix, version=version)
+ with in_env(prefix, version):
+ # Need to call this before installing so rbenv's directories are
+ # set up
+ helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-'))
+ if version != C.DEFAULT:
+ _install_ruby(prefix, version)
+ # Need to call this after installing to set up the shims
+ helpers.run_setup_cmd(prefix, ('rbenv', 'rehash'))
+ helpers.run_setup_cmd(
+ prefix, ('gem', 'build', *prefix.star('.gemspec')),
+ )
+ helpers.run_setup_cmd(
+ prefix,
+ (
+ 'gem', 'install', '--no-document',
+ *prefix.star('.gem'), *additional_dependencies,
+ ),
+ )
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]: # pragma: win32 no cover
+ with in_env(hook.prefix, hook.language_version):
+ return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py
new file mode 100644
index 0000000..7ea3f54
--- /dev/null
+++ b/pre_commit/languages/rust.py
@@ -0,0 +1,106 @@
+import contextlib
+import os.path
+from typing import Generator
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+
+import toml
+
+import pre_commit.constants as C
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import PatchesT
+from pre_commit.envcontext import Var
+from pre_commit.hook import Hook
+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
+
+ENVIRONMENT_DIR = 'rustenv'
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+
+
+def get_env_patch(target_dir: str) -> PatchesT:
+ return (
+ ('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))),
+ )
+
+
+@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)):
+ yield
+
+
+def _add_dependencies(
+ cargo_toml_path: str,
+ 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()
+
+
+def install_environment(
+ prefix: Prefix,
+ version: str,
+ additional_dependencies: Sequence[str],
+) -> None:
+ helpers.assert_version_default('rust', version)
+ directory = prefix.path(
+ helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
+ )
+
+ # There are two cases where we might want to specify more dependencies:
+ # as dependencies for the library being built, and as binary packages
+ # to be `cargo install`'d.
+ #
+ # Unlike e.g. Python, if we just `cargo install` a library, it won't be
+ # used for compilation. And if we add a crate providing a binary to the
+ # `Cargo.toml`, the binary won't be built.
+ #
+ # Because of this, we allow specifying "cli" dependencies by prefixing
+ # with 'cli:'.
+ cli_deps = {
+ dep for dep in additional_dependencies if dep.startswith('cli:')
+ }
+ 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))
+ 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,
+ )
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]:
+ with in_env(hook.prefix):
+ return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py
new file mode 100644
index 0000000..a5e1365
--- /dev/null
+++ b/pre_commit/languages/script.py
@@ -0,0 +1,19 @@
+from typing import Sequence
+from typing import Tuple
+
+from pre_commit.hook import Hook
+from pre_commit.languages import helpers
+
+ENVIRONMENT_DIR = None
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+install_environment = helpers.no_install
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]:
+ cmd = (hook.prefix.path(hook.cmd[0]), *hook.cmd[1:])
+ return helpers.run_xargs(hook, cmd, file_args, color=color)
diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py
new file mode 100644
index 0000000..66aadc8
--- /dev/null
+++ b/pre_commit/languages/swift.py
@@ -0,0 +1,64 @@
+import contextlib
+import os
+from typing import Generator
+from typing import Sequence
+from typing import Tuple
+
+import pre_commit.constants as C
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import PatchesT
+from pre_commit.envcontext import Var
+from pre_commit.hook import Hook
+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
+
+ENVIRONMENT_DIR = 'swift_env'
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+BUILD_DIR = '.build'
+BUILD_CONFIG = 'release'
+
+
+def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover
+ bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG)
+ return (('PATH', (bin_path, os.pathsep, Var('PATH'))),)
+
+
+@contextlib.contextmanager # pragma: win32 no cover
+def in_env(prefix: Prefix) -> Generator[None, None, None]:
+ envdir = prefix.path(
+ helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
+ )
+ with envcontext(get_env_patch(envdir)):
+ yield
+
+
+def install_environment(
+ prefix: Prefix, version: str, additional_dependencies: Sequence[str],
+) -> None: # pragma: win32 no cover
+ helpers.assert_version_default('swift', version)
+ helpers.assert_no_additional_deps('swift', additional_dependencies)
+ directory = prefix.path(
+ helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
+ )
+
+ # Build the swift package
+ with clean_path_on_failure(directory):
+ os.mkdir(directory)
+ cmd_output_b(
+ 'swift', 'build',
+ '-C', prefix.prefix_dir,
+ '-c', BUILD_CONFIG,
+ '--build-path', os.path.join(directory, BUILD_DIR),
+ )
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]: # pragma: win32 no cover
+ with in_env(hook.prefix):
+ return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py
new file mode 100644
index 0000000..139f45d
--- /dev/null
+++ b/pre_commit/languages/system.py
@@ -0,0 +1,19 @@
+from typing import Sequence
+from typing import Tuple
+
+from pre_commit.hook import Hook
+from pre_commit.languages import helpers
+
+
+ENVIRONMENT_DIR = None
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+install_environment = helpers.no_install
+
+
+def run_hook(
+ hook: Hook,
+ file_args: Sequence[str],
+ color: bool,
+) -> Tuple[int, bytes]:
+ return helpers.run_xargs(hook, hook.cmd, file_args, color=color)