diff --git a/.travis.yml b/.travis.yml index 8f91d702..9327173f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,8 @@ before_install: fi - git --version - './get-swift.sh && export PATH="/tmp/swift/usr/bin:$PATH"' + - 'curl -sSf https://sh.rustup.rs | bash -s -- -y' + - export PATH="$HOME/.cargo/bin:$PATH" after_success: coveralls cache: directories: diff --git a/appveyor.yml b/appveyor.yml index ddb9af3c..772caf4d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,6 +11,8 @@ install: - pip install tox virtualenv --upgrade - "mkdir -p C:\\Temp" - "SET TMPDIR=C:\\Temp" + - "curl -sSf https://sh.rustup.rs | bash -s -- -y" + - "SET PATH=%USERPROFILE%\\.cargo\\bin;%PATH%" # Not a C# project build: false diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 504c28a0..be74ffd3 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -9,6 +9,7 @@ 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 @@ -60,6 +61,7 @@ languages = { 'python': python, 'python_venv': python_venv, 'ruby': ruby, + 'rust': rust, 'script': script, 'swift': swift, 'system': system, diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py new file mode 100644 index 00000000..41053f88 --- /dev/null +++ b/pre_commit/languages/rust.py @@ -0,0 +1,94 @@ +from __future__ import unicode_literals + +import contextlib +import os.path + +import toml + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import Var +from pre_commit.languages import helpers +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output +from pre_commit.xargs import xargs + + +ENVIRONMENT_DIR = 'rustenv' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(target_dir): + return ( + ( + 'PATH', + (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH')), + ), + ) + + +@contextlib.contextmanager +def in_env(prefix): + target_dir = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, 'default'), + ) + with envcontext(get_env_patch(target_dir)): + yield + + +def _add_dependencies(cargo_toml_path, additional_dependencies): + 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, version, additional_dependencies): + helpers.assert_version_default('rust', version) + directory = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, '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 = {()} + 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 package in packages_to_install: + cmd_output( + 'cargo', 'install', '--bins', '--root', directory, *package, + cwd=prefix.prefix_dir + ) + + +def run_hook(prefix, hook, file_args): + with in_env(prefix): + return xargs(helpers.to_cmd(hook), file_args) diff --git a/pre_commit/resources/empty_template/Cargo.toml b/pre_commit/resources/empty_template/Cargo.toml new file mode 100644 index 00000000..3dfeffaf --- /dev/null +++ b/pre_commit/resources/empty_template/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "__fake_crate" +version = "0.0.0" + +[[bin]] +name = "__fake_cmd" +path = "main.rs" diff --git a/pre_commit/resources/empty_template/main.rs b/pre_commit/resources/empty_template/main.rs new file mode 100644 index 00000000..f328e4d9 --- /dev/null +++ b/pre_commit/resources/empty_template/main.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/setup.py b/setup.py index c4504774..831dc000 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ setup( 'nodeenv>=0.11.1', 'pyyaml', 'six', + 'toml', 'virtualenv', ], entry_points={ diff --git a/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..df1269ff --- /dev/null +++ b/testing/resources/rust_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: rust-hook + name: rust example hook + entry: rust-hello-world + language: rust + files: '' diff --git a/testing/resources/rust_hooks_repo/Cargo.lock b/testing/resources/rust_hooks_repo/Cargo.lock new file mode 100644 index 00000000..36fbfda2 --- /dev/null +++ b/testing/resources/rust_hooks_repo/Cargo.lock @@ -0,0 +1,3 @@ +[[package]] +name = "rust-hello-world" +version = "0.1.0" diff --git a/testing/resources/rust_hooks_repo/Cargo.toml b/testing/resources/rust_hooks_repo/Cargo.toml new file mode 100644 index 00000000..cd83b435 --- /dev/null +++ b/testing/resources/rust_hooks_repo/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "rust-hello-world" +version = "0.1.0" diff --git a/testing/resources/rust_hooks_repo/src/main.rs b/testing/resources/rust_hooks_repo/src/main.rs new file mode 100644 index 00000000..ad379d6e --- /dev/null +++ b/testing/resources/rust_hooks_repo/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("hello world"); +} diff --git a/tests/repository_test.py b/tests/repository_test.py index 67b8f3f6..2ca399ce 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -20,6 +20,7 @@ from pre_commit.languages import node from pre_commit.languages import pcre from pre_commit.languages import python from pre_commit.languages import ruby +from pre_commit.languages import rust from pre_commit.repository import Repository from pre_commit.util import cmd_output from testing.fixtures import config_with_local_hooks @@ -282,6 +283,55 @@ def test_golang_hook(tempdir_factory, store): ) +@pytest.mark.integration +def test_rust_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'rust_hooks_repo', + 'rust-hook', [], b'hello world\n', + ) + + +@pytest.mark.integration +@pytest.mark.parametrize('dep', ('cli:shellharden:3.1.0', 'cli:shellharden')) +def test_additional_rust_cli_dependencies_installed( + tempdir_factory, store, dep, +): + path = make_repo(tempdir_factory, 'rust_hooks_repo') + config = make_config_from_repo(path) + # A small rust package with no dependencies. + config['hooks'][0]['additional_dependencies'] = [dep] + repo = Repository.create(config, store) + repo.require_installed() + (prefix, _, _, _), = repo._venvs() + binaries = os.listdir(prefix.path( + helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', + )) + # normalize for windows + binaries = [os.path.splitext(binary)[0] for binary in binaries] + assert 'shellharden' in binaries + + +@pytest.mark.integration +def test_additional_rust_lib_dependencies_installed( + tempdir_factory, store, +): + path = make_repo(tempdir_factory, 'rust_hooks_repo') + config = make_config_from_repo(path) + # A small rust package with no dependencies. + deps = ['shellharden:3.1.0'] + config['hooks'][0]['additional_dependencies'] = deps + repo = Repository.create(config, store) + repo.require_installed() + (prefix, _, _, _), = repo._venvs() + binaries = os.listdir(prefix.path( + helpers.environment_dir(rust.ENVIRONMENT_DIR, 'default'), 'bin', + )) + # normalize for windows + binaries = [os.path.splitext(binary)[0] for binary in binaries] + assert 'rust-hello-world' in binaries + assert 'shellharden' not in binaries + + @pytest.mark.integration def test_missing_executable(tempdir_factory, store): _test_hook_repo( @@ -554,6 +604,24 @@ def test_local_golang_additional_dependencies(store): assert _norm_out(ret[1]) == b"Hello, Go examples!\n" +def test_local_rust_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'hello', + 'name': 'hello', + 'entry': 'hello', + 'language': 'rust', + 'additional_dependencies': ['cli:hello-cli:0.2.2'], + }], + } + repo = Repository.create(config, store) + (_, hook), = repo.hooks + ret = repo.run_hook(hook, ()) + assert ret[0] == 0 + assert _norm_out(ret[1]) == b"Hello World!\n" + + def test_reinstall(tempdir_factory, store, log_info_mock): path = make_repo(tempdir_factory, 'python_hooks_repo') config = make_config_from_repo(path)