From f8e21cb78bbc885eb0afe12ec69b5358e44ddf89 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 28 Aug 2021 18:44:34 -0400 Subject: [PATCH] add support for dart as a hook language --- azure-pipelines.yml | 26 ++--- pre_commit/languages/all.py | 2 + pre_commit/languages/dart.py | 109 ++++++++++++++++++ pre_commit/languages/helpers.py | 4 +- pre_commit/languages/python.py | 3 +- .../resources/empty_template_pubspec.yaml | 4 + pre_commit/store.py | 2 +- pre_commit/util.py | 4 + testing/gen-languages-all | 6 +- testing/get-coursier.sh | 4 +- testing/get-dart.sh | 17 +++ testing/get-swift.sh | 4 +- .../dart_repo/.pre-commit-hooks.yaml | 4 + .../dart_repo/bin/hello-world-dart.dart | 6 + testing/resources/dart_repo/pubspec.yaml | 10 ++ .../.pre-commit-hooks.yaml | 2 +- .../.pre-commit-hooks.yaml | 2 +- tests/languages/python_test.py | 5 +- tests/repository_test.py | 42 ++++++- 19 files changed, 227 insertions(+), 29 deletions(-) create mode 100644 pre_commit/languages/dart.py create mode 100644 pre_commit/resources/empty_template_pubspec.yaml create mode 100755 testing/get-dart.sh create mode 100644 testing/resources/dart_repo/.pre-commit-hooks.yaml create mode 100644 testing/resources/dart_repo/bin/hello-world-dart.dart create mode 100644 testing/resources/dart_repo/pubspec.yaml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 58dee74a8..a468e8aa6 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -26,9 +26,9 @@ jobs: Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin" Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin" displayName: Add strawberry perl to PATH - - task: PowerShell@2 - inputs: - filePath: "testing/get-r.ps1" + - bash: testing/get-dart.sh + displayName: install dart + - powershell: testing/get-r.ps1 displayName: install R - template: job--python-tox.yml@asottile parameters: @@ -38,13 +38,11 @@ jobs: pre_test: - task: UseRubyVersion@0 - template: step--git-install.yml - - bash: | - testing/get-coursier.sh - echo '##vso[task.prependpath]/tmp/coursier' + - bash: testing/get-coursier.sh displayName: install coursier - - bash: | - testing/get-swift.sh - echo '##vso[task.prependpath]/tmp/swift/usr/bin' + - bash: testing/get-dart.sh + displayName: install dart + - bash: testing/get-swift.sh displayName: install swift - bash: testing/get-r.sh displayName: install R @@ -54,13 +52,11 @@ jobs: os: linux pre_test: - task: UseRubyVersion@0 - - bash: | - testing/get-coursier.sh - echo '##vso[task.prependpath]/tmp/coursier' + - bash: testing/get-coursier.sh displayName: install coursier - - bash: | - testing/get-swift.sh - echo '##vso[task.prependpath]/tmp/swift/usr/bin' + - bash: testing/get-dart.sh + displayName: install dart + - bash: testing/get-swift.sh displayName: install swift - bash: testing/get-r.sh displayName: install R diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index fde6000cb..d8a364c5d 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -7,6 +7,7 @@ from pre_commit.hook import Hook from pre_commit.languages import conda from pre_commit.languages import coursier +from pre_commit.languages import dart from pre_commit.languages import docker from pre_commit.languages import docker_image from pre_commit.languages import dotnet @@ -44,6 +45,7 @@ class Language(NamedTuple): # 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 'coursier': Language(name='coursier', ENVIRONMENT_DIR=coursier.ENVIRONMENT_DIR, get_default_version=coursier.get_default_version, healthy=coursier.healthy, install_environment=coursier.install_environment, run_hook=coursier.run_hook), # noqa: E501 + 'dart': Language(name='dart', ENVIRONMENT_DIR=dart.ENVIRONMENT_DIR, get_default_version=dart.get_default_version, healthy=dart.healthy, install_environment=dart.install_environment, run_hook=dart.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 'dotnet': Language(name='dotnet', ENVIRONMENT_DIR=dotnet.ENVIRONMENT_DIR, get_default_version=dotnet.get_default_version, healthy=dotnet.healthy, install_environment=dotnet.install_environment, run_hook=dotnet.run_hook), # noqa: E501 diff --git a/pre_commit/languages/dart.py b/pre_commit/languages/dart.py new file mode 100644 index 000000000..16e755461 --- /dev/null +++ b/pre_commit/languages/dart.py @@ -0,0 +1,109 @@ +import contextlib +import os.path +import shutil +import tempfile +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 win_exe +from pre_commit.util import yaml_load + +ENVIRONMENT_DIR = 'dartenv' + +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]: + directory = helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT) + 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('dart', version) + + envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version)) + bin_dir = os.path.join(envdir, 'bin') + + def _install_dir(prefix_p: Prefix, pub_cache: str) -> None: + dart_env = {**os.environ, 'PUB_CACHE': pub_cache} + + with open(prefix_p.path('pubspec.yaml')) as f: + pubspec_contents = yaml_load(f) + + helpers.run_setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env) + + for executable in pubspec_contents['executables']: + helpers.run_setup_cmd( + prefix_p, + ( + 'dart', 'compile', 'exe', + '--output', os.path.join(bin_dir, win_exe(executable)), + prefix_p.path('bin', f'{executable}.dart'), + ), + env=dart_env, + ) + + with clean_path_on_failure(envdir): + os.makedirs(bin_dir) + + with tempfile.TemporaryDirectory() as tmp: + _install_dir(prefix, tmp) + + for dep_s in additional_dependencies: + with tempfile.TemporaryDirectory() as dep_tmp: + dep, _, version = dep_s.partition(':') + if version: + dep_cmd: Tuple[str, ...] = (dep, '--version', version) + else: + dep_cmd = (dep,) + + helpers.run_setup_cmd( + prefix, + ('dart', 'pub', 'cache', 'add', *dep_cmd), + env={**os.environ, 'PUB_CACHE': dep_tmp}, + ) + + # try and find the 'pubspec.yaml' that just got added + for root, _, filenames in os.walk(dep_tmp): + if 'pubspec.yaml' in filenames: + with tempfile.TemporaryDirectory() as copied: + pkg = os.path.join(copied, 'pkg') + shutil.copytree(root, pkg) + _install_dir(Prefix(pkg), dep_tmp) + break + else: + raise AssertionError( + f'could not find pubspec.yaml for {dep_s}', + ) + + +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 index 29138fd1a..276ce1611 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -48,8 +48,8 @@ def exe_exists(exe: str) -> bool: ) -def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None: - cmd_output_b(*cmd, cwd=prefix.prefix_dir) +def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...], **kwargs: Any) -> None: + cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs) @overload diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py index 43b728082..faa602977 100644 --- a/pre_commit/languages/python.py +++ b/pre_commit/languages/python.py @@ -21,6 +21,7 @@ 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 win_exe ENVIRONMENT_DIR = 'py_env' @@ -172,7 +173,7 @@ def healthy(prefix: Prefix, language_version: str) -> bool: if not os.path.exists(pyvenv_cfg): return False - exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + exe_name = win_exe('python') py_exe = prefix.path(bin_dir(envdir), exe_name) cfg = _read_pyvenv_cfg(pyvenv_cfg) diff --git a/pre_commit/resources/empty_template_pubspec.yaml b/pre_commit/resources/empty_template_pubspec.yaml new file mode 100644 index 000000000..3be6ffe36 --- /dev/null +++ b/pre_commit/resources/empty_template_pubspec.yaml @@ -0,0 +1,4 @@ +name: pre_commit_empty_pubspec +environment: + sdk: '>=2.10.0' +executables: {} diff --git a/pre_commit/store.py b/pre_commit/store.py index 0fd5e6238..fc3bc511a 100644 --- a/pre_commit/store.py +++ b/pre_commit/store.py @@ -189,7 +189,7 @@ def _git_cmd(*args: str) -> None: LOCAL_RESOURCES = ( 'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore', 'package.json', 'pre_commit_placeholder_package.gemspec', 'setup.py', - 'environment.yml', 'Makefile.PL', + 'environment.yml', 'Makefile.PL', 'pubspec.yaml', 'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv', ) diff --git a/pre_commit/util.py b/pre_commit/util.py index b5f40ada4..6bf8ae7a1 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -268,3 +268,7 @@ def handle_remove_readonly( def parse_version(s: str) -> Tuple[int, ...]: """poor man's version comparison""" return tuple(int(p) for p in s.split('.')) + + +def win_exe(s: str) -> str: + return s if sys.platform != 'win32' else f'{s}.exe' diff --git a/testing/gen-languages-all b/testing/gen-languages-all index eb7cd701e..51e4bce62 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -2,9 +2,9 @@ import sys LANGUAGES = [ - 'conda', 'coursier', 'docker', 'docker_image', 'dotnet', 'fail', 'golang', - 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', 'script', - 'swift', 'system', + 'conda', 'coursier', 'dart', 'docker', 'docker_image', 'dotnet', 'fail', + 'golang', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', + 'script', 'swift', 'system', ] FIELDS = [ 'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment', diff --git a/testing/get-coursier.sh b/testing/get-coursier.sh index 760c6c125..4c5e955de 100755 --- a/testing/get-coursier.sh +++ b/testing/get-coursier.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This is a script used in CI to install coursier -set -euxo pipefail +set -euo pipefail COURSIER_URL="https://github.com/coursier/coursier/releases/download/v2.0.0/cs-x86_64-pc-linux" COURSIER_HASH="e2e838b75bc71b16bcb77ce951ad65660c89bda7957c79a0628ec7146d35122f" @@ -11,3 +11,5 @@ rm -f "$ARTIFACT" curl --location --silent --output "$ARTIFACT" "$COURSIER_URL" echo "$COURSIER_HASH $ARTIFACT" | sha256sum --check chmod ugo+x /tmp/coursier/cs + +echo '##vso[task.prependpath]/tmp/coursier' diff --git a/testing/get-dart.sh b/testing/get-dart.sh new file mode 100755 index 000000000..b655e1a8d --- /dev/null +++ b/testing/get-dart.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION=2.13.4 + +if [ "$OSTYPE" = msys ]; then + URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip" + echo "##vso[task.prependpath]$(cygpath -w /tmp/dart-sdk/bin)" +else + URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-linux-x64-release.zip" + echo '##vso[task.prependpath]/tmp/dart-sdk/bin' +fi + +curl --silent --location --output /tmp/dart.zip "$URL" + +unzip -q -d /tmp /tmp/dart.zip +rm /tmp/dart.zip diff --git a/testing/get-swift.sh b/testing/get-swift.sh index e205d44e2..a05b7b9e6 100755 --- a/testing/get-swift.sh +++ b/testing/get-swift.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This is a script used in CI to install swift -set -euxo pipefail +set -euo pipefail . /etc/lsb-release if [ "$DISTRIB_CODENAME" = "bionic" ]; then @@ -25,3 +25,5 @@ fi mkdir -p /tmp/swift tar -xf "$TGZ" --strip 1 --directory /tmp/swift + +echo '##vso[task.prependpath]/tmp/swift/usr/bin' diff --git a/testing/resources/dart_repo/.pre-commit-hooks.yaml b/testing/resources/dart_repo/.pre-commit-hooks.yaml new file mode 100644 index 000000000..e0dc5a2a9 --- /dev/null +++ b/testing/resources/dart_repo/.pre-commit-hooks.yaml @@ -0,0 +1,4 @@ +- id: hello-world-dart + name: hello world dart + entry: hello-world-dart + language: dart diff --git a/testing/resources/dart_repo/bin/hello-world-dart.dart b/testing/resources/dart_repo/bin/hello-world-dart.dart new file mode 100644 index 000000000..5d8d6a6af --- /dev/null +++ b/testing/resources/dart_repo/bin/hello-world-dart.dart @@ -0,0 +1,6 @@ +import 'package:ansicolor/ansicolor.dart'; + +void main() { + AnsiPen pen = new AnsiPen()..red(); + print("hello hello " + pen("world")); +} diff --git a/testing/resources/dart_repo/pubspec.yaml b/testing/resources/dart_repo/pubspec.yaml new file mode 100644 index 000000000..bc719d055 --- /dev/null +++ b/testing/resources/dart_repo/pubspec.yaml @@ -0,0 +1,10 @@ +environment: + sdk: '>=2.10.0 <3.0.0' + +name: hello_world_dart + +executables: + hello-world-dart: + +dependencies: + ansicolor: ^2.0.1 diff --git a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml index d005a74cc..0f514c116 100644 --- a/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml +++ b/testing/resources/dotnet_hooks_csproj_repo/.pre-commit-hooks.yaml @@ -1,4 +1,4 @@ -- id: dotnet example hook +- id: dotnet-example-hook name: dotnet example hook entry: testeroni language: dotnet diff --git a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml index d005a74cc..0f514c116 100644 --- a/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml +++ b/testing/resources/dotnet_hooks_sln_repo/.pre-commit-hooks.yaml @@ -1,4 +1,4 @@ -- id: dotnet example hook +- id: dotnet-example-hook name: dotnet example hook entry: testeroni language: dotnet diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 90d1036a3..8324cac20 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -9,6 +9,7 @@ from pre_commit.languages import python from pre_commit.prefix import Prefix from pre_commit.util import make_executable +from pre_commit.util import win_exe def test_read_pyvenv_cfg(tmpdir): @@ -112,7 +113,7 @@ def test_unhealthy_python_goes_missing(python_dir): python.install_environment(prefix, C.DEFAULT, ()) - exe_name = 'python' if sys.platform != 'win32' else 'python.exe' + exe_name = win_exe('python') py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) os.remove(py_exe) @@ -158,7 +159,7 @@ def test_unhealthy_then_replaced(python_dir): python.install_environment(prefix, C.DEFAULT, ()) # simulate an exe which returns an old version - exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + exe_name = win_exe('python') py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name) os.rename(py_exe, f'{py_exe}.tmp') diff --git a/tests/repository_test.py b/tests/repository_test.py index af829c2e7..e372519a5 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1043,10 +1043,50 @@ def test_local_perl_additional_dependencies(store): def test_dotnet_hook(tempdir_factory, store, repo): _test_hook_repo( tempdir_factory, store, repo, - 'dotnet example hook', [], b'Hello from dotnet!\n', + 'dotnet-example-hook', [], b'Hello from dotnet!\n', ) +def test_dart_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'dart_repo', + 'hello-world-dart', [], b'hello hello world\n', + ) + + +def test_local_dart_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-dart', + 'name': 'local-dart', + 'entry': 'hello-world-dart', + 'language': 'dart', + 'additional_dependencies': ['hello_world_dart'], + }], + } + hook = _get_hook(config, store, 'local-dart') + ret, out = _hook_run(hook, (), color=False) + assert (ret, _norm_out(out)) == (0, b'hello hello world\n') + + +def test_local_dart_additional_dependencies_versioned(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-dart', + 'name': 'local-dart', + 'entry': 'secure-random -l 4 -b 16', + 'language': 'dart', + 'additional_dependencies': ['encrypt:5.0.0'], + }], + } + hook = _get_hook(config, store, 'local-dart') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + re_assert.Matches('^[a-f0-9]{8}\r?\n$').assert_matches(out.decode()) + + def test_non_installable_hook_error_for_language_version(store, caplog): config = { 'repo': 'local',