├── requirements-dev.txt ├── requirements-dev-minimal.txt ├── requirements-minimal.txt ├── requirements.txt ├── setup.cfg ├── bin ├── set-tag ├── build ├── push ├── _info ├── info └── test ├── languages └── R │ ├── build.sh │ └── Dockerfile ├── LICENSE ├── .github └── workflows │ └── main.yml ├── .pre-commit-config.yaml ├── README.md └── Dockerfile /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-dev-minimal.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-minimal.txt: -------------------------------------------------------------------------------- 1 | pre-commit>=2.7.0 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cfgv==3.5.0 2 | distlib==0.4.0 3 | filelock==3.20.0 4 | identify==2.6.15 5 | nodeenv==1.9.1 6 | platformdirs==4.5.0 7 | pre-commit==4.5.0 8 | pyyaml==6.0.3 9 | virtualenv==20.35.4 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [mypy] 2 | check_untyped_defs = true 3 | disallow_any_generics = true 4 | disallow_incomplete_defs = true 5 | disallow_untyped_defs = true 6 | warn_redundant_casts = true 7 | warn_unused_ignores = true 8 | 9 | [mypy-testing.*] 10 | disallow_untyped_defs = false 11 | 12 | [mypy-tests.*] 13 | disallow_untyped_defs = false 14 | -------------------------------------------------------------------------------- /bin/set-tag: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import datetime 5 | import subprocess 6 | 7 | 8 | def main() -> int: 9 | rev_parse = ('git', 'rev-parse', '--short', 'HEAD') 10 | revision = subprocess.check_output(rev_parse, text=True).strip() 11 | print(f'TAG=runner-image:{datetime.date.today()}-{revision}') 12 | return 0 13 | 14 | 15 | if __name__ == '__main__': 16 | raise SystemExit(main()) 17 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import argparse 5 | import subprocess 6 | 7 | 8 | def main() -> int: 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument('tag') 11 | args = parser.parse_args() 12 | 13 | subprocess.check_call(( 14 | 'docker', 'build', 15 | '--pull', 16 | '--cache-from', 'ghcr.io/pre-commit-ci/runner-image:latest-full', 17 | '--cache-to', 'type=inline', 18 | '--tag', f'{args.tag}-full', 19 | '.', 20 | )) 21 | subprocess.check_call(( 22 | 'docker', 'build', 23 | '--tag', args.tag, 24 | '--target', 'minimal', 25 | '.', 26 | )) 27 | 28 | return 0 29 | 30 | 31 | if __name__ == '__main__': 32 | raise SystemExit(main()) 33 | -------------------------------------------------------------------------------- /languages/R/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | R_VERSION=4.4.2 5 | R_SHA256=1578cd603e8d866b58743e49d8bf99c569e81079b6a60cf33cdf7bdffeb817ec 6 | # https://www.r-project.org/ 7 | 8 | cd "$(dirname "$0")" 9 | 10 | podman build \ 11 | --build-arg=R_VERSION="$R_VERSION" \ 12 | --build-arg=R_SHA256="$R_SHA256" \ 13 | -t runner-image-r . 14 | 15 | cid="$(podman create runner-image-r sleep infinity)" 16 | trap 'podman rm -f "$cid"' exit 17 | 18 | podman start "$cid" 19 | podman exec "$cid" rm -rf /opt/r/lib/R/doc /opt/r/share 20 | podman exec "$cid" \ 21 | tar -C /opt/r \ 22 | --sort=name \ 23 | --mtime="1970-01-01 00:00:00Z" \ 24 | --owner=0 --group=0 --numeric-owner \ 25 | -czf /tmp/r.tgz . 26 | 27 | podman cp "$cid:/tmp/r.tgz" "r-${R_VERSION}.tgz" 28 | sha256sum "r-${R_VERSION}.tgz" 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Anthony Sottile 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /languages/R/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:noble 2 | RUN : \ 3 | && sed -i 's/^Types: deb$/Types: deb deb-src/g' /etc/apt/sources.list.d/ubuntu.sources \ 4 | && apt-get update \ 5 | && DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends build-dep r-base \ 6 | && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl \ 7 | && apt-get clean \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | ARG R_VERSION 11 | ARG R_SHA256 12 | RUN : \ 13 | && set -x \ 14 | && mkdir /tmp/r \ 15 | && cd /tmp/r \ 16 | && curl --silent --location --output r.tgz "https://cran.rstudio.com/src/base/R-${R_VERSION%%.*}/R-$R_VERSION.tar.gz" \ 17 | && echo "${R_SHA256} r.tgz" | sha256sum --check \ 18 | && tar -xf r.tgz \ 19 | && cd "R-${R_VERSION}" \ 20 | && mkdir -p /opt/r/ \ 21 | && ./configure \ 22 | --prefix=/opt/r/ \ 23 | --enable-memory-profiling \ 24 | --enable-R-shlib \ 25 | --with-blas \ 26 | --with-lapack \ 27 | --without-recommended-packages \ 28 | && make \ 29 | && make install \ 30 | && rm -rf /tmp/r \ 31 | && : 32 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | push: 4 | branches: [main] 5 | 6 | permissions: 7 | contents: write 8 | id-token: write 9 | packages: write 10 | 11 | jobs: 12 | main: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.11' 19 | - run: bin/set-tag >> "$GITHUB_ENV" 20 | - run: bin/build "$TAG" 21 | - run: bin/test "$TAG" 22 | - uses: actions/checkout@v3 23 | with: 24 | ref: info 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | path: info 27 | if: github.event_name != 'pull_request' 28 | - uses: pre-commit-ci/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 29 | with: 30 | role-to-assume: ${{ secrets.ECR_DEPLOY_ROLE_ARN }} 31 | aws-region: us-east-1 32 | if: github.event_name != 'pull_request' 33 | - run: bin/push info "$TAG" 34 | env: 35 | GHCR_USER: ${{ github.actor }} 36 | GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | if: github.event_name != 'pull_request' 38 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v6.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: debug-statements 9 | - id: double-quote-string-fixer 10 | - id: name-tests-test 11 | - id: requirements-txt-fixer 12 | - repo: https://github.com/asottile/reorder-python-imports 13 | rev: v3.16.0 14 | hooks: 15 | - id: reorder-python-imports 16 | args: [--py311-plus, --add-import, 'from __future__ import annotations'] 17 | - repo: https://github.com/asottile/add-trailing-comma 18 | rev: v4.0.0 19 | hooks: 20 | - id: add-trailing-comma 21 | - repo: https://github.com/asottile/pyupgrade 22 | rev: v3.21.2 23 | hooks: 24 | - id: pyupgrade 25 | args: [--py311-plus] 26 | - repo: https://github.com/hhatto/autopep8 27 | rev: v2.3.2 28 | hooks: 29 | - id: autopep8 30 | - repo: https://github.com/PyCQA/flake8 31 | rev: 7.3.0 32 | hooks: 33 | - id: flake8 34 | - repo: https://github.com/pre-commit/mirrors-mypy 35 | rev: v1.19.1 36 | hooks: 37 | - id: mypy 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/pre-commit-ci/runner-image/actions/workflows/main.yml/badge.svg)](https://github.com/pre-commit-ci/runner-image/actions) 2 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/pre-commit-ci/runner-image/main.svg)](https://results.pre-commit.ci/latest/github/pre-commit-ci/runner-image/main) 3 | 4 | runner-image 5 | ============ 6 | 7 | This is the image that is used to build and run in pre-commit.ci. 8 | 9 | ### contributing new languages 10 | 11 | additional languages used to impact the scaling time of pre-commit.ci but 12 | are now factored in a way that they can be lazily loaded. this requires 13 | special care in the Dockerfile to make sure those languages function correctly. 14 | 15 | a language consists of a few things after the `echo: 'end minimal'` marker: 16 | 17 | - a single `ENV` instruction which sets up the environment variables for 18 | running that language 19 | - a single `RUN` instruction which installs the language into `/opt/${lang}` 20 | - the `RUN` instruction must also contain `echo 'lang: ${lang}'` such that 21 | the lazy loading machinery can identify where the language is. 22 | - the value of `${lang}` must match the `language` field for pre-commit. 23 | 24 | `swift` is one example language that is set up in this way. 25 | -------------------------------------------------------------------------------- /bin/push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import argparse 5 | import os.path 6 | import subprocess 7 | import sys 8 | 9 | HERE = os.path.dirname(__file__) 10 | 11 | 12 | def _push( 13 | *, 14 | minimal_tag: str, 15 | full_tag: str, 16 | hostname: str, 17 | username: str, 18 | password: str, 19 | target: str, 20 | ) -> None: 21 | subprocess.run( 22 | ( 23 | 'docker', 'login', '--username', username, '--password-stdin', 24 | hostname, 25 | ), 26 | input=password.encode(), 27 | check=True, 28 | ) 29 | 30 | _, _, minimal_version = minimal_tag.partition(':') 31 | _, _, full_version = full_tag.partition(':') 32 | 33 | to_push = ( 34 | (minimal_tag, f'{target}:{minimal_version}'), 35 | (minimal_tag, f'{target}:latest'), 36 | (full_tag, f'{target}:{full_version}'), 37 | (full_tag, f'{target}:latest-full'), 38 | ) 39 | 40 | for src, target in to_push: 41 | subprocess.check_call(('docker', 'tag', src, target)) 42 | subprocess.check_call(('docker', 'push', target)) 43 | 44 | 45 | def main() -> int: 46 | parser = argparse.ArgumentParser() 47 | parser.add_argument('git_dir') 48 | parser.add_argument('tag') 49 | args = parser.parse_args() 50 | 51 | minimal_tag = args.tag 52 | full_tag = f'{args.tag}-full' 53 | 54 | aws_password_cmd = ('aws', 'ecr-public', 'get-login-password') 55 | ecr_password = subprocess.check_output(aws_password_cmd).strip().decode() 56 | 57 | _push( 58 | minimal_tag=minimal_tag, 59 | full_tag=full_tag, 60 | hostname='public.ecr.aws', 61 | username='AWS', 62 | password=ecr_password, 63 | target='public.ecr.aws/k7o0k5z0/pre-commit-ci-runner-image', 64 | ) 65 | 66 | _push( 67 | minimal_tag=minimal_tag, 68 | full_tag=full_tag, 69 | hostname='ghcr.io', 70 | username=os.environ['GHCR_USER'], 71 | password=os.environ['GHCR_TOKEN'], 72 | target='ghcr.io/pre-commit-ci/runner-image', 73 | ) 74 | 75 | cmd = (sys.executable, '-uS', os.path.join(HERE, 'info'), full_tag) 76 | with open(os.path.join(args.git_dir, 'versions.md'), 'wb') as f: 77 | subprocess.check_call(cmd, stdout=f) 78 | 79 | git = ( 80 | 'git', 81 | '-c', 'user.name=github-actions', 82 | '-c', 'user.email=41898282+github-actions[bot]@users.noreply.github.com', # noqa: E501 83 | '-C', args.git_dir, 84 | ) 85 | subprocess.check_call((*git, 'add', '.')) 86 | msg = f'update versions for {args.tag}' 87 | subprocess.check_call((*git, 'commit', '-q', '-m', msg)) 88 | subprocess.check_call((*git, 'tag', args.tag.replace(':', '_'))) 89 | subprocess.check_call((*git, 'push', '-q', '--tags', 'origin', 'HEAD')) 90 | 91 | return 0 92 | 93 | 94 | if __name__ == '__main__': 95 | raise SystemExit(main()) 96 | -------------------------------------------------------------------------------- /bin/_info: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import contextlib 5 | import shlex 6 | import subprocess 7 | from collections.abc import Generator 8 | 9 | 10 | @contextlib.contextmanager 11 | def _console() -> Generator[None]: 12 | print('```console') 13 | try: 14 | yield 15 | finally: 16 | print('```') 17 | 18 | 19 | def _call(*cmd: str) -> None: 20 | print(f'$ {shlex.join(cmd)}') 21 | for line in subprocess.check_output(cmd).decode().splitlines(): 22 | print(line.rstrip()) 23 | 24 | 25 | def main() -> int: 26 | print('## pre-commit') 27 | print() 28 | with _console(): 29 | _call('pip', 'freeze', '--all') 30 | print() 31 | 32 | print('## os') 33 | print() 34 | with _console(): 35 | _call('cat', '/etc/lsb-release') 36 | print() 37 | 38 | print('## python') 39 | print() 40 | print('default `python` / `python3`') 41 | print() 42 | with _console(): 43 | _call('python', '--version', '--version') 44 | print() 45 | _call('python3', '--version', '--version') 46 | print() 47 | print('others') 48 | print() 49 | with _console(): 50 | _call('python3.10', '--version', '--version') 51 | print() 52 | _call('python3.11', '--version', '--version') 53 | print() 54 | _call('python3.13', '--version', '--version') 55 | print() 56 | _call('python3.14', '--version', '--version') 57 | print() 58 | _call('pypy3', '--version', '--version') 59 | print() 60 | 61 | print('## conda') 62 | print() 63 | with _console(): 64 | _call('conda', '--version') 65 | print() 66 | 67 | print('## coursier') 68 | print() 69 | with _console(): 70 | _call('cs', 'version') 71 | _call('java', '--version') 72 | print() 73 | 74 | print('## dart') 75 | print() 76 | with _console(): 77 | _call('dart', '--version') 78 | print() 79 | 80 | print('## dotnet') 81 | print() 82 | with _console(): 83 | _call('dotnet', '--info') 84 | print() 85 | 86 | print('## go') 87 | print() 88 | with _console(): 89 | _call('go', 'version') 90 | print() 91 | 92 | print('## lua') 93 | print() 94 | with _console(): 95 | _call('lua', '-v') 96 | print() 97 | _call('luarocks', '--version') 98 | print() 99 | 100 | print('## node') 101 | print() 102 | with _console(): 103 | _call('node', '--version') 104 | print() 105 | _call('npm', '--version') 106 | print() 107 | 108 | print('## perl') 109 | print() 110 | with _console(): 111 | _call('perl', '-E', 'print "$^V\n"') 112 | print() 113 | 114 | print('## r') 115 | print() 116 | with _console(): 117 | _call('R', '--version') 118 | print() 119 | 120 | print('## ruby') 121 | print() 122 | with _console(): 123 | _call('ruby', '--version') 124 | print() 125 | 126 | print('## rust') 127 | print() 128 | with _console(): 129 | _call('cargo', '--version') 130 | print() 131 | _call('rustc', '--version') 132 | print() 133 | 134 | print('## swift') 135 | print() 136 | with _console(): 137 | _call('swift', '-version') 138 | 139 | return 0 140 | 141 | 142 | if __name__ == '__main__': 143 | raise SystemExit(main()) 144 | -------------------------------------------------------------------------------- /bin/info: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import argparse 5 | import ast 6 | import functools 7 | import json 8 | import os.path 9 | import subprocess 10 | import urllib.error 11 | import urllib.parse 12 | import urllib.request 13 | from collections.abc import Mapping 14 | 15 | HERE = os.path.dirname(__file__) 16 | 17 | 18 | def _parse_auth_header(s: str) -> dict[str, str]: 19 | bearer = 'Bearer ' 20 | assert s.startswith(bearer) 21 | s = s[len(bearer):] 22 | 23 | ret = {} 24 | for part in s.split(','): 25 | k, _, v = part.partition('=') 26 | v = ast.literal_eval(v) 27 | ret[k] = v 28 | return ret 29 | 30 | 31 | @functools.cache 32 | def _auth_challenge(registry: str) -> tuple[str, Mapping[str, str]]: 33 | try: 34 | urllib.request.urlopen(f'https://{registry}/v2/', timeout=5) 35 | except urllib.error.HTTPError as e: 36 | if e.code != 401 or 'www-authenticate' not in e.headers: 37 | raise 38 | 39 | auth = _parse_auth_header(e.headers['www-authenticate']) 40 | else: 41 | raise AssertionError(f'expected auth challenge: {registry}') 42 | 43 | realm = auth.pop('realm') 44 | auth.setdefault('scope', 'repository:user/image:pull') 45 | 46 | return realm, auth 47 | 48 | 49 | def _digest_impl(registry: str, image: str, tag: str) -> str: 50 | realm, auth = _auth_challenge(registry) 51 | auth = {k: v.replace('user/image', image) for k, v in auth.items()} 52 | 53 | auth_url = f'{realm}?{urllib.parse.urlencode(auth)}' 54 | token = json.load(urllib.request.urlopen(auth_url))['token'] 55 | 56 | req = urllib.request.Request( 57 | f'https://{registry}/v2/{image}/manifests/{tag}', 58 | headers={ 59 | 'Authorization': f'Bearer {token}', 60 | 'Accept': 'application/vnd.docker.distribution.manifest.v2+json', 61 | }, 62 | method='HEAD', 63 | ) 64 | resp = urllib.request.urlopen(req) 65 | return resp.headers['Docker-Content-Digest'] 66 | 67 | 68 | def _digest(img: str) -> str: 69 | base, tag = img.split(':') 70 | registry, image = base.split('/', 1) 71 | return _digest_impl(registry, image, tag) 72 | 73 | 74 | def main() -> int: 75 | parser = argparse.ArgumentParser() 76 | parser.add_argument('tag') 77 | args = parser.parse_args() 78 | 79 | full_tag = args.tag 80 | tag = full_tag.removesuffix('-full') 81 | 82 | src = f'''\ 83 | {full_tag} 84 | {'=' * len(full_tag)} 85 | 86 | to pull this image: 87 | 88 | ```bash 89 | docker pull ghcr.io/pre-commit-ci/{full_tag} 90 | ``` 91 | 92 | digests: 93 | 94 | ```python 95 | IMAGE = {tag!r} 96 | DIGESTS = ( 97 | Image( 98 | name='public.ecr.aws/k7o0k5z0/pre-commit-ci-runner-image', 99 | minimal={_digest(f'public.ecr.aws/k7o0k5z0/pre-commit-ci-{tag}')!r}, # noqa: E501 100 | full={_digest(f'public.ecr.aws/k7o0k5z0/pre-commit-ci-{full_tag}')!r}, # noqa: E501 101 | ), 102 | Image( 103 | name='ghcr.io/pre-commit-ci/runner-image', 104 | minimal={_digest(f'ghcr.io/pre-commit-ci/{tag}')!r}, # noqa: E501 105 | full={_digest(f'ghcr.io/pre-commit-ci/{full_tag}')!r}, # noqa: E501 106 | ), 107 | ) 108 | ``` 109 | ''' 110 | print(src) 111 | 112 | with open(os.path.join(HERE, '_info'), 'rb') as f: 113 | contents = f.read() 114 | 115 | cmd = ('docker', 'run', '--rm', '-i', args.tag, 'python3', '-uS', '-') 116 | ret = subprocess.run(cmd, input=contents, stderr=subprocess.STDOUT) 117 | return ret.returncode 118 | 119 | 120 | if __name__ == '__main__': 121 | raise SystemExit(main()) 122 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:noble AS minimal 2 | 3 | ENTRYPOINT ["dumb-init", "--"] 4 | 5 | RUN : \ 6 | && apt-get update \ 7 | && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 8 | bzip2 \ 9 | ca-certificates \ 10 | cmake \ 11 | curl \ 12 | dumb-init \ 13 | g++ \ 14 | gcc \ 15 | git \ 16 | gnupg2 \ 17 | libblas3 \ 18 | libc6 \ 19 | libedit2 \ 20 | libffi-dev \ 21 | libfile-homedir-perl \ 22 | libgcc1 \ 23 | libgdiplus \ 24 | libgssapi-krb5-2 \ 25 | libicu74 \ 26 | liblapack3 \ 27 | libssl3 \ 28 | libstdc++6 \ 29 | libtirpc3t64 \ 30 | libxml2 \ 31 | libyaml-dev \ 32 | libz3-dev \ 33 | make \ 34 | python3-dev \ 35 | ruby-dev \ 36 | unzip \ 37 | xdg-user-dirs \ 38 | zlib1g \ 39 | && apt-get clean \ 40 | && rm -rf /var/lib/apt/lists/* \ 41 | && : 42 | 43 | RUN : \ 44 | && . /etc/lsb-release \ 45 | && apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys F23C5A6CF475977595C89F51BA6932366A755776 \ 46 | && echo deb http://ppa.launchpad.net/deadsnakes/ppa/ubuntu $DISTRIB_CODENAME main > /etc/apt/sources.list.d/deadsnakes.list \ 47 | && apt-get update \ 48 | && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 49 | pypy3-dev \ 50 | python3.10-dev \ 51 | python3.10-distutils \ 52 | python3.11-dev \ 53 | python3.11-distutils \ 54 | python3.13-dev \ 55 | python3.14-dev \ 56 | && apt-get clean \ 57 | && rm -rf /var/lib/apt/lists/* \ 58 | && : 59 | 60 | ENV \ 61 | PATH=/venv/bin:$PATH \ 62 | PRE_COMMIT_HOME=/pc \ 63 | npm_config_cache=/tmp/npm \ 64 | PIP_DISABLE_PIP_VERSION_CHECK=1 \ 65 | PIP_NO_CACHE_DIR=1 \ 66 | PIP_ONLY_BINARY=numpy,pandas,scipy,scikit-learn \ 67 | VIRTUALENV_ACTIVATORS=bash \ 68 | VIRTUALENV_NO_PERIODIC_UPDATE=1 \ 69 | VIRTUALENV_PIP=embed \ 70 | VIRTUALENV_SETUPTOOLS=embed \ 71 | VIRTUALENV_WHEEL=embed \ 72 | XDG_CACHE_HOME=/tmp/cache \ 73 | XDG_DATA_HOME=/tmp/data 74 | COPY requirements.txt /tmp/requirements.txt 75 | RUN : \ 76 | && curl --silent --location --output /tmp/virtualenv.pyz https://bootstrap.pypa.io/virtualenv/3.10/virtualenv.pyz \ 77 | && python3.12 /tmp/virtualenv.pyz /venv \ 78 | && pip install --requirement /tmp/requirements.txt \ 79 | && rm -rf "$XDG_DATA_HOME" /tmp/virtualenv.pyz \ 80 | && : 81 | 82 | # ensure virtualenv appdata cache is populated 83 | ENV \ 84 | VIRTUALENV_OVERRIDE_APP_DATA=/opt/virtualenv/cache \ 85 | VIRTUALENV_SYMLINK_APP_DATA=1 86 | COPY build/* /tmp/ 87 | RUN /tmp/seed-virtualenv-cache 88 | ENV VIRTUALENV_READ_ONLY_APP_DATA=1 89 | 90 | # pre-commit.ci requires cross-user readonly `/src` repo access 91 | RUN git config --system --add safe.directory /src 92 | 93 | ARG GO=1.25.3 94 | ARG GO_SHA256=0335f314b6e7bfe08c3d0cfaa7c19db961b7b99fb20be62b0a826c992ad14e0f 95 | ENV PATH=/opt/go/bin:$PATH GOFLAGS=-modcacherw 96 | RUN : \ 97 | && mkdir -p /opt \ 98 | && curl --location --silent --output go.tgz https://golang.org/dl/go${GO}.linux-amd64.tar.gz \ 99 | && echo "${GO_SHA256} go.tgz" | sha256sum --check \ 100 | && tar -C /opt -xf go.tgz \ 101 | && rm -rf /opt/go/doc /opt/go/test \ 102 | && rm go.tgz 103 | 104 | RUN echo 'end: minimal' 105 | 106 | FROM minimal AS final 107 | 108 | ARG NODE=20.19.2 109 | ARG NODE_SHA256=eec2c7b9c6ac72e42885a42edfc0503c0e4ee455f855c4a17a6cbcf026656dd5 110 | ENV PATH=/opt/node/bin:$PATH 111 | RUN : \ 112 | && echo 'lang: node' \ 113 | && curl --silent --location --output /tmp/node.tar.gz "https://nodejs.org/dist/v${NODE}/node-v${NODE}-linux-x64.tar.gz" \ 114 | && echo "${NODE_SHA256} /tmp/node.tar.gz" | sha256sum --check \ 115 | && mkdir /opt/node \ 116 | && tar --strip-components 1 --directory /opt/node -xf /tmp/node.tar.gz \ 117 | && rm /tmp/node.tar.gz \ 118 | && : 119 | 120 | ARG RUST=1.89.0 121 | ARG RUSTUP_SHA256=c8d03f559a2335693379e1d3eaee76622b2a6580807e63bcd61faea709b9f664 122 | ARG RUSTUP_VERSION=1.28.0 123 | ENV \ 124 | CARGO_HOME=/tmp/cargo/home \ 125 | RUSTUP_HOME=/opt/rust/rustup \ 126 | PATH=/opt/rust/cargo/bin:$PATH 127 | RUN : \ 128 | && echo 'lang: rust' \ 129 | && export CARGO_HOME=/opt/rust/cargo \ 130 | && rustArch='x86_64-unknown-linux-gnu' \ 131 | && curl --silent --location --output rustup-init "https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${rustArch}/rustup-init" \ 132 | && echo "${RUSTUP_SHA256} rustup-init" | sha256sum --check \ 133 | && chmod +x rustup-init \ 134 | && ./rustup-init -y --profile minimal --no-modify-path --default-toolchain "$RUST" --default-host "$rustArch" \ 135 | && rm -rf rustup-init \ 136 | && rustup component add clippy rustfmt \ 137 | && : 138 | 139 | ARG SWIFT=6.0.3 140 | ARG SWIFT_SHA256=33e923609f6d89ee455af0a017ae4941ce16878c4940882cbf6a1656de294e8b 141 | ENV \ 142 | PATH=/opt/swift/usr/bin:$PATH \ 143 | LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/swift/usr/lib 144 | RUN : \ 145 | && echo 'lang: swift' \ 146 | && . /etc/lsb-release \ 147 | && curl --silent --location --output /tmp/swift.tar.gz https://download.swift.org/swift-$SWIFT-release/ubuntu$(echo $DISTRIB_RELEASE | tr -d ".")/swift-$SWIFT-RELEASE/swift-$SWIFT-RELEASE-ubuntu$DISTRIB_RELEASE.tar.gz \ 148 | && echo "${SWIFT_SHA256} /tmp/swift.tar.gz" | sha256sum --check \ 149 | && mkdir /opt/swift \ 150 | && tar --strip-components 1 --directory /opt/swift -xf /tmp/swift.tar.gz \ 151 | && rm /tmp/swift.tar.gz \ 152 | && : 153 | 154 | ARG DOTNET_URL=https://download.visualstudio.microsoft.com/download/pr/7fe73a07-575d-4cb4-b2d3-c23d89e5085f/d8b2b7e1c0ed99c1144638d907c6d152/dotnet-sdk-7.0.101-linux-x64.tar.gz 155 | ARG DOTNET_SHA512=cf289ad0e661c38dcda7f415b3078a224e8347528448429d62c0f354ee951f4e7bef9cceaf3db02fb52b5dd7be987b7a4327ca33fb9239b667dc1c41c678095c 156 | ENV \ 157 | PATH=/opt/dotnet:$PATH \ 158 | DOTNET_ROOT=/opt/dotnet \ 159 | DOTNET_CLI_HOME=/tmp \ 160 | DOTNET_CLI_TELEMETRY_OPTOUT=1 \ 161 | DOTNET_NOLOGO=1 \ 162 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 163 | RUN : \ 164 | && echo 'lang: dotnet' \ 165 | && dotnet_root=/opt/dotnet \ 166 | && mkdir -p $dotnet_root \ 167 | && curl \ 168 | --location \ 169 | --fail \ 170 | --silent \ 171 | --output /tmp/dotnet.tar.gz \ 172 | "$DOTNET_URL" \ 173 | && echo "${DOTNET_SHA512} /tmp/dotnet.tar.gz" | sha512sum --check \ 174 | && tar -C $dotnet_root -xf /tmp/dotnet.tar.gz \ 175 | && rm /tmp/dotnet.tar.gz \ 176 | && : 177 | 178 | ARG CONDA=py39_4.10.3 179 | ARG CONDA_SHA256=1ea2f885b4dbc3098662845560bc64271eb17085387a70c2ba3f29fff6f8d52f 180 | ENV PATH=/opt/conda/bin:$PATH CONDA_PKGS_DIRS=/tmp/conda/pkgs 181 | RUN : \ 182 | && echo 'lang: conda' \ 183 | && curl --silent --location --output /tmp/conda.sh "https://repo.anaconda.com/miniconda/Miniconda3-$CONDA-Linux-x86_64.sh" \ 184 | && bash /tmp/conda.sh -p /opt/conda/install -b \ 185 | && mkdir /opt/conda/bin \ 186 | && ln -sf /opt/conda/install/bin/conda /opt/conda/bin \ 187 | && rm -rf /tmp/conda.sh /root/.conda \ 188 | && : 189 | 190 | ARG DART=2.13.4 191 | ARG DART_SHA256=633a9aa4812b725ff587e2bbf16cd5839224cfe05dcd536e1a74804e80fdb4cd 192 | ENV PATH=/opt/dart/dart-sdk/bin:$PATH 193 | RUN : \ 194 | && echo 'lang: dart' \ 195 | && curl --silent --location --output /tmp/dart.zip "https://storage.googleapis.com/dart-archive/channels/stable/release/${DART}/sdk/dartsdk-linux-x64-release.zip" \ 196 | && echo "${DART_SHA256} /tmp/dart.zip" | sha256sum --check \ 197 | && mkdir /opt/dart \ 198 | && unzip -q -d /opt/dart /tmp/dart.zip \ 199 | # permissions are wrong in the archive? 200 | # https://github.com/dart-lang/sdk/issues/47093 201 | && chmod -R og+rX /opt/dart \ 202 | && rm /tmp/dart.zip \ 203 | && : 204 | 205 | ENV \ 206 | PATH=/opt/r/bin/:$PATH \ 207 | RENV_CONFIG_CACHE_ENABLED=false \ 208 | RENV_CONFIG_CACHE_SYMLINKS=false \ 209 | RENV_PATHS_ROOT=/tmp/renv 210 | RUN : \ 211 | && echo 'lang: r' \ 212 | && curl --silent --location --output /tmp/r.tgz https://github.com/pre-commit-ci/runner-image/releases/download/ubuntu-24.04-r-4.4.2/r-4.4.2.tgz \ 213 | && echo '735db5e00a1f69970a490c77183daa1d7323ff7bb6b7637860e760779c90e889 /tmp/r.tgz' | sha256sum --check \ 214 | && mkdir /opt/r \ 215 | && tar -C /opt/r -xf /tmp/r.tgz \ 216 | && rm /tmp/r.tgz \ 217 | && : 218 | 219 | ARG LUA=5.4.3 220 | ARG LUA_SHA256=f8612276169e3bfcbcfb8f226195bfc6e466fe13042f1076cbde92b7ec96bbfb 221 | ARG LUAROCKS=3.8.0 222 | ARG LUAROCKS_SHA256=56ab9b90f5acbc42eb7a94cf482e6c058a63e8a1effdf572b8b2a6323a06d923 223 | ENV PATH=/opt/lua/bin:$PATH 224 | RUN : \ 225 | && echo 'lang: lua' \ 226 | && curl --location --silent --output /tmp/lua.tgz "https://www.lua.org/ftp/lua-${LUA}.tar.gz" \ 227 | && echo "${LUA_SHA256} /tmp/lua.tgz" | sha256sum --check \ 228 | && curl --location --silent --output /tmp/luarocks.tgz "https://luarocks.org/releases/luarocks-${LUAROCKS}.tar.gz" \ 229 | && echo "${LUAROCKS_SHA256} /tmp/luarocks.tgz" | sha256sum --check \ 230 | && tar -C /tmp --strip-components=1 --one-top-level -xf /tmp/lua.tgz \ 231 | && make -C /tmp/lua INSTALL_TOP=/opt/lua all \ 232 | && make -C /tmp/lua INSTALL_TOP=/opt/lua install \ 233 | && tar -C /tmp --strip-components=1 --one-top-level -xf /tmp/luarocks.tgz \ 234 | && cd /tmp/luarocks \ 235 | && ./configure --prefix=/opt/lua \ 236 | && make install \ 237 | && rm -rf /tmp/lua /tmp/luarocks /tmp/lua.tgz /tmp/luarocks.tgz 238 | 239 | ARG CS=v2.1.0-RC6 240 | ARG CS_SHA256=ef2bc32c8d1975d9373f518ee24ecbd9a96e99cbb523afa309a45cb44009eeb7 241 | ARG JDK_SHA256=aef49cc7aa606de2044302e757fa94c8e144818e93487081c4fd319ca858134b 242 | ENV PATH=/opt/coursier/bin:$PATH 243 | RUN : \ 244 | && echo 'lang: coursier' \ 245 | && curl --location --silent --output /tmp/cs.gz "https://github.com/coursier/coursier/releases/download/${CS}/cs-x86_64-pc-linux.gz" \ 246 | && echo "${CS_SHA256} /tmp/cs.gz" | sha256sum --check \ 247 | && curl --location --silent --output /tmp/jdk.tgz "https://download.java.net/openjdk/jdk17/ri/openjdk-17+35_linux-x64_bin.tar.gz" \ 248 | && echo "${JDK_SHA256} /tmp/jdk.tgz" | sha256sum --check \ 249 | && mkdir -p /opt/coursier \ 250 | && tar --strip-components=1 -C /opt/coursier -xf /tmp/jdk.tgz \ 251 | && gunzip /tmp/cs.gz \ 252 | && mv /tmp/cs /opt/coursier/bin \ 253 | && chmod +x /opt/coursier/bin/cs \ 254 | && rm /tmp/jdk.tgz 255 | 256 | RUN : \ 257 | && echo 'lang: meta' \ 258 | && /tmp/language-info --dest /opt/meta 259 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import annotations 3 | 4 | import argparse 5 | import os.path 6 | import subprocess 7 | import tempfile 8 | 9 | 10 | PYTHON_HOOKS = '''\ 11 | repos: 12 | - repo: local 13 | hooks: 14 | - id: flake8-local 15 | name: flake8-local 16 | language: python 17 | entry: flake8 18 | additional_dependencies: [flake8] 19 | types: [python] 20 | - id: flake8-local-lang 21 | name: flake8-local 22 | language: python 23 | entry: flake8 24 | additional_dependencies: [flake8] 25 | types: [python] 26 | language_version: python3.10 27 | ''' 28 | 29 | 30 | def _build_and_run(tag: str, tmpdir: str, pc: str, *, podman: bool) -> None: 31 | userns = ('--userns=keep-id',) if podman else () 32 | subprocess.check_call(( 33 | 'docker', 'run', '--rm', 34 | '--user', f'{os.getuid()}:{os.getgid()}', 35 | *userns, 36 | '--volume', f'{tmpdir}:/git:ro', 37 | '--volume', f'{pc}:/pc:rw', 38 | '--workdir', '/git', 39 | tag, 'python3', '-mpre_commit', 'install-hooks', 40 | )) 41 | subprocess.check_call(( 42 | 'docker', 'run', '--rm', 43 | '--user', f'{os.getuid()}:{os.getgid()}', 44 | *userns, 45 | '--volume', f'{tmpdir}:/git:rw', 46 | '--volume', f'{pc}:/pc:ro', 47 | '--workdir', '/git', 48 | tag, 'python3', '-mpre_commit', 'run', '--all-files', 49 | )) 50 | 51 | 52 | def test_can_run_python_tools(tag: str, pc: str, *, podman: bool) -> None: 53 | with tempfile.TemporaryDirectory() as tmpdir: 54 | subprocess.check_call(('git', 'init', tmpdir)) 55 | with open(os.path.join(tmpdir, '.pre-commit-config.yaml'), 'w') as f: 56 | f.write(PYTHON_HOOKS) 57 | with open(os.path.join(tmpdir, 't.py'), 'w') as f: 58 | f.write("print('hello hello world')\n") 59 | subprocess.check_call(('git', '-C', tmpdir, 'add', '.')) 60 | 61 | _build_and_run(tag, tmpdir, pc, podman=podman) 62 | 63 | 64 | def test_considers_system_ruby_suitable(tag: str) -> None: 65 | output = subprocess.check_output(( 66 | 'docker', 'run', '--rm', 67 | tag, 'python3', '-c', 68 | 'import pre_commit.languages.ruby;' 69 | 'print(pre_commit.languages.ruby.get_default_version())', 70 | )) 71 | assert output == b'system\n', output 72 | 73 | 74 | def test_considers_system_node_suitable(tag: str) -> None: 75 | output = subprocess.check_output(( 76 | 'docker', 'run', '--rm', 77 | tag, 'python3', '-c', 78 | 'import pre_commit.languages.node;' 79 | 'print(pre_commit.languages.node.get_default_version())', 80 | )) 81 | assert output == b'system\n', output 82 | 83 | 84 | NODE_HOOK = '''\ 85 | repos: 86 | - repo: local 87 | hooks: 88 | - id: prettier 89 | name: prettier 90 | entry: prettier 91 | types: [javascript] 92 | language: node 93 | additional_dependencies: [prettier@v2.1.2] 94 | ''' 95 | 96 | 97 | def test_can_run_node_hook(tag: str, pc: str, podman: bool) -> None: 98 | with tempfile.TemporaryDirectory() as tmpdir: 99 | subprocess.check_call(('git', 'init', tmpdir)) 100 | with open(os.path.join(tmpdir, '.pre-commit-config.yaml'), 'w') as f: 101 | f.write(NODE_HOOK) 102 | with open(os.path.join(tmpdir, 't.js'), 'w') as f: 103 | f.write("console.log('hello world');") 104 | subprocess.check_call(('git', '-C', tmpdir, 'add', '.')) 105 | 106 | _build_and_run(tag, tmpdir, pc, podman=podman) 107 | 108 | 109 | def test_can_install_ffi_gem(tag: str) -> None: 110 | subprocess.check_call(( 111 | 'docker', 'run', '--rm', tag, 112 | 'gem', 'install', '--install-dir=/tmp', '--no-document', 'ffi', 113 | )) 114 | 115 | 116 | GO_HOOK = '''\ 117 | repos: 118 | - repo: https://github.com/golangci/golangci-lint 119 | rev: v1.33.0 120 | hooks: 121 | - id: golangci-lint 122 | ''' 123 | 124 | GOLANGCI_LINT_CONFIG = '''\ 125 | linters: 126 | disable-all: true 127 | enable: 128 | - gofmt 129 | run: 130 | deadline: 30s 131 | ''' 132 | 133 | 134 | def test_can_run_go_hook(tag: str, pc: str, podman: bool) -> None: 135 | with tempfile.TemporaryDirectory() as tmpdir: 136 | subprocess.check_call(('git', 'init', tmpdir)) 137 | with open(os.path.join(tmpdir, '.pre-commit-config.yaml'), 'w') as f: 138 | f.write(GO_HOOK) 139 | with open(os.path.join(tmpdir, '.golangci.yml'), 'w') as f: 140 | f.write(GOLANGCI_LINT_CONFIG) 141 | with open(os.path.join(tmpdir, 'pkg.go'), 'w') as f: 142 | f.write('package somepkg\n') 143 | with open(os.path.join(tmpdir, 'go.mod'), 'w') as f: 144 | f.write('module github.com/example/mod\n\ngo 1.15\n') 145 | subprocess.check_call(('git', '-C', tmpdir, 'add', '.')) 146 | 147 | _build_and_run(tag, tmpdir, pc, podman=podman) 148 | 149 | 150 | RUST_HOOK = '''\ 151 | repos: 152 | - repo: local 153 | hooks: 154 | - id: rust-local 155 | name: rust-local 156 | language: rust 157 | entry: shellharden 158 | types: [shell] 159 | additional_dependencies: [cli:shellharden:3.1.0] 160 | ''' 161 | 162 | 163 | def test_can_run_rust_hook(tag: str, pc: str, podman: bool) -> None: 164 | with tempfile.TemporaryDirectory() as tmpdir: 165 | subprocess.check_call(('git', 'init', tmpdir)) 166 | with open(os.path.join(tmpdir, '.pre-commit-config.yaml'), 'w') as f: 167 | f.write(RUST_HOOK) 168 | with open(os.path.join(tmpdir, 'test.sh'), 'w') as f: 169 | f.write('echo hi \n') 170 | subprocess.check_call(('git', '-C', tmpdir, 'add', '.')) 171 | 172 | _build_and_run(tag, tmpdir, pc, podman=podman) 173 | 174 | 175 | SWIFT_HOOK = '''\ 176 | repos: 177 | - repo: https://github.com/nicklockwood/SwiftFormat 178 | rev: 0.47.8 179 | hooks: 180 | - id: swiftformat 181 | ''' 182 | 183 | PLACEHOLDER_SWIFT_FILE = '''\ 184 | class Dummy {} 185 | ''' 186 | 187 | 188 | def test_can_run_swift_hook(tag: str, pc: str, podman: bool) -> None: 189 | with tempfile.TemporaryDirectory() as tmpdir: 190 | subprocess.check_call(('git', 'init', tmpdir)) 191 | with open(os.path.join(tmpdir, '.pre-commit-config.yaml'), 'w') as f: 192 | f.write(SWIFT_HOOK) 193 | with open(os.path.join(tmpdir, 'Dummy.swift'), 'w') as f: 194 | f.write(PLACEHOLDER_SWIFT_FILE) 195 | subprocess.check_call(('git', '-C', tmpdir, 'add', '.')) 196 | 197 | _build_and_run(tag, tmpdir, pc, podman=podman) 198 | 199 | 200 | DOTNET_HOOK = '''\ 201 | repos: 202 | # switch back to rkm once pre-commit 2.21 lands 203 | - repo: https://github.com/pre-commit-ci/sample-dotnet-tool 204 | rev: asottile-patch-1 205 | hooks: 206 | - id: sample-dotnet-tool 207 | ''' 208 | 209 | PLACEHOLDER_DOTNET_CSPROJ_FILE = '''\ 210 | 211 | 212 | Exe 213 | net5.0 214 | 215 | 216 | ''' 217 | 218 | PLACEHOLDER_DOTNET_PROGRAM_FILE = '''\ 219 | using System; 220 | namespace Testeroni 221 | { 222 | class Program 223 | { 224 | static void Main(string[] args) 225 | { 226 | Console.WriteLine("Hello Hello World!"); 227 | } 228 | } 229 | } 230 | ''' 231 | 232 | 233 | def test_can_run_dotnet_hook(tag: str, pc: str, podman: bool) -> None: 234 | with tempfile.TemporaryDirectory() as tmpdir: 235 | subprocess.check_call(('git', 'init', tmpdir)) 236 | with open(os.path.join(tmpdir, '.pre-commit-config.yaml'), 'w') as f: 237 | f.write(DOTNET_HOOK) 238 | with open(os.path.join(tmpdir, 'Testeroni.csproj'), 'w') as f: 239 | f.write(PLACEHOLDER_DOTNET_CSPROJ_FILE) 240 | with open(os.path.join(tmpdir, 'Program.cs'), 'w') as f: 241 | f.write(PLACEHOLDER_DOTNET_PROGRAM_FILE) 242 | subprocess.check_call(('git', '-C', tmpdir, 'add', '.')) 243 | 244 | _build_and_run(tag, tmpdir, pc, podman=podman) 245 | 246 | 247 | CONDA_HOOK = '''\ 248 | repos: 249 | - repo: local 250 | hooks: 251 | - id: pyupgrade-conda 252 | name: pyupgrade conda 253 | entry: pyupgrade 254 | language: conda 255 | types: [python] 256 | additional_dependencies: [-c, conda-forge, pyupgrade] 257 | ''' 258 | 259 | 260 | def test_can_run_conda_hook(tag: str, pc: str, podman: bool) -> None: 261 | with tempfile.TemporaryDirectory() as tmpdir: 262 | subprocess.check_call(('git', 'init', tmpdir)) 263 | with open(os.path.join(tmpdir, '.pre-commit-config.yaml'), 'w') as f: 264 | f.write(CONDA_HOOK) 265 | with open(os.path.join(tmpdir, 't.py'), 'w') as f: 266 | f.write('print("hello world")') 267 | subprocess.check_call(('git', '-C', tmpdir, 'add', '.')) 268 | 269 | _build_and_run(tag, tmpdir, pc, podman=podman) 270 | 271 | 272 | DART_HOOK = '''\ 273 | repos: 274 | - repo: local 275 | hooks: 276 | - id: hello-world-dart 277 | name: hello world dart 278 | entry: hello-world-dart 279 | language: dart 280 | additional_dependencies: [hello_world_dart] 281 | verbose: true 282 | ''' 283 | 284 | 285 | def test_can_run_dart_hook(tag: str, pc: str, podman: bool) -> None: 286 | with tempfile.TemporaryDirectory() as tmpdir: 287 | subprocess.check_call(('git', 'init', tmpdir)) 288 | with open(os.path.join(tmpdir, '.pre-commit-config.yaml'), 'w') as f: 289 | f.write(DART_HOOK) 290 | subprocess.check_call(('git', '-C', tmpdir, 'add', '.')) 291 | 292 | _build_and_run(tag, tmpdir, pc, podman=podman) 293 | 294 | 295 | R_HOOK = '''\ 296 | repos: 297 | - repo: local 298 | hooks: 299 | - id: r-local 300 | name: r-local 301 | language: r 302 | entry: Rscript -e "glue::glue('1+1')" 303 | additional_dependencies: [glue@1.4.2] 304 | ''' 305 | 306 | 307 | def test_can_run_r_hook(tag: str, pc: str, podman: bool) -> None: 308 | with tempfile.TemporaryDirectory() as tmpdir: 309 | subprocess.check_call(('git', 'init', tmpdir)) 310 | with open(os.path.join(tmpdir, '.pre-commit-config.yaml'), 'w') as f: 311 | f.write(R_HOOK) 312 | subprocess.check_call(('git', '-C', tmpdir, 'add', '.')) 313 | 314 | _build_and_run(tag, tmpdir, pc, podman=podman) 315 | 316 | 317 | LUA_HOOK = '''\ 318 | repos: 319 | - repo: local 320 | hooks: 321 | - id: lua-local 322 | name: lua-local 323 | language: lua 324 | entry: luacheck 325 | additional_dependencies: [luacheck] 326 | types: [lua] 327 | ''' 328 | 329 | 330 | def test_can_run_lua_hook(tag: str, pc: str, podman: bool) -> None: 331 | with tempfile.TemporaryDirectory() as tmpdir: 332 | subprocess.check_call(('git', 'init', tmpdir)) 333 | with open(os.path.join(tmpdir, '.pre-commit-config.yaml'), 'w') as f: 334 | f.write(LUA_HOOK) 335 | with open(os.path.join(tmpdir, 't.lua'), 'w') as f: 336 | f.write("print('hello world')") 337 | subprocess.check_call(('git', '-C', tmpdir, 'add', '.')) 338 | 339 | _build_and_run(tag, tmpdir, pc, podman=podman) 340 | 341 | 342 | PERL_HOOK = '''\ 343 | repos: 344 | - repo: https://github.com/cmhughes/latexindent.pl 345 | rev: V3.19.1 346 | hooks: 347 | - id: latexindent 348 | ''' 349 | 350 | 351 | def test_can_run_perl_hook(tag: str, pc: str, podman: bool) -> None: 352 | with tempfile.TemporaryDirectory() as tmpdir: 353 | subprocess.check_call(('git', 'init', tmpdir)) 354 | with open(os.path.join(tmpdir, '.pre-commit-config.yaml'), 'w') as f: 355 | f.write(PERL_HOOK) 356 | tex = r'''\ 357 | \begin{document} 358 | hello world 359 | \end{document} 360 | ''' 361 | with open(os.path.join(tmpdir, 't.tex'), 'w') as f: 362 | f.write(tex) 363 | subprocess.check_call(('git', '-C', tmpdir, 'add', '.')) 364 | 365 | _build_and_run(tag, tmpdir, pc, podman=podman) 366 | 367 | 368 | def test_can_run_coursier_hook(tag: str, pc: str, podman: bool) -> None: 369 | coursier_hook = '''\ 370 | repos: 371 | - repo: local 372 | hooks: 373 | - id: coursier-hook 374 | name: coursier-hook 375 | entry: scalafmt 376 | language: coursier 377 | additional_dependencies: ['scalafmt:3.6.1'] 378 | types: [scala] 379 | ''' 380 | hello_world = '''\ 381 | object Hello { 382 | def main(args: Array[String]) = { 383 | println("Hello, world") 384 | } 385 | } 386 | ''' 387 | with tempfile.TemporaryDirectory() as tmpdir: 388 | subprocess.check_call(('git', 'init', tmpdir)) 389 | with open(os.path.join(tmpdir, '.pre-commit-config.yaml'), 'w') as f: 390 | f.write(coursier_hook) 391 | with open(os.path.join(tmpdir, '.scalafmt.conf'), 'w') as f: 392 | f.write('version = 3.6.1\nrunner.dialect = scala3\n') 393 | t_scala = os.path.join(tmpdir, 't.scala') 394 | with open(t_scala, 'w') as f: 395 | f.write(hello_world) 396 | subprocess.check_call(('git', '-C', tmpdir, 'add', '.')) 397 | 398 | _build_and_run(tag, tmpdir, pc, podman=podman) 399 | 400 | 401 | def main() -> int: 402 | parser = argparse.ArgumentParser() 403 | parser.add_argument('--podman', action='store_true') 404 | parser.add_argument('tag') 405 | args = parser.parse_args() 406 | 407 | minimal_tag = args.tag 408 | full_tag = f'{args.tag}-full' 409 | 410 | with tempfile.TemporaryDirectory() as pc: 411 | print(' can run python tools '.center(79, '='), flush=True) 412 | test_can_run_python_tools(minimal_tag, pc, podman=args.podman) 413 | print(' considers system ruby suitable '.center(79, '='), flush=True) 414 | test_considers_system_ruby_suitable(full_tag) 415 | print(' condiders system node suitable '.center(79, '='), flush=True) 416 | test_considers_system_node_suitable(full_tag) 417 | print(' can run node hook '.center(79, '='), flush=True) 418 | test_can_run_node_hook(full_tag, pc, podman=args.podman) 419 | print(' can install ffi gem '.center(79, '='), flush=True) 420 | test_can_install_ffi_gem(full_tag) 421 | print(' can run go hooks '.center(79, '='), flush=True) 422 | test_can_run_go_hook(full_tag, pc, podman=args.podman) 423 | print(' can run rust hooks '.center(79, '='), flush=True) 424 | test_can_run_rust_hook(full_tag, pc, podman=args.podman) 425 | print(' can run swift hooks '.center(79, '='), flush=True) 426 | test_can_run_swift_hook(full_tag, pc, podman=args.podman) 427 | print(' can run dotnet hooks '.center(79, '='), flush=True) 428 | test_can_run_dotnet_hook(full_tag, pc, podman=args.podman) 429 | print(' can run conda hooks '.center(79, '='), flush=True) 430 | test_can_run_conda_hook(full_tag, pc, podman=args.podman) 431 | print(' can run dart tools '.center(79, '='), flush=True) 432 | test_can_run_dart_hook(full_tag, pc, podman=args.podman) 433 | print(' can run R hooks '.center(79, '='), flush=True) 434 | test_can_run_r_hook(full_tag, pc, podman=args.podman) 435 | print(' can run lua hooks '.center(79, '='), flush=True) 436 | test_can_run_lua_hook(full_tag, pc, podman=args.podman) 437 | print(' can run perl hooks '.center(79, '='), flush=True) 438 | test_can_run_perl_hook(full_tag, pc, podman=args.podman) 439 | print(' can run coursier hooks '.center(79, '='), flush=True) 440 | test_can_run_coursier_hook(full_tag, pc, podman=args.podman) 441 | 442 | return 0 443 | 444 | 445 | if __name__ == '__main__': 446 | raise SystemExit(main()) 447 | --------------------------------------------------------------------------------