diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 614170ba7..a3b4863ef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,3 +42,7 @@ repos: - id: mypy additional_dependencies: [types-pyyaml] exclude: ^testing/resources/ +- repo: https://github.com/fredrikekre/runic-pre-commit + rev: fe/julia + hooks: + - id: runic-julia diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py index f2d11bb60..ba569c377 100644 --- a/pre_commit/all_languages.py +++ b/pre_commit/all_languages.py @@ -10,6 +10,7 @@ from pre_commit.languages import fail from pre_commit.languages import golang from pre_commit.languages import haskell +from pre_commit.languages import julia from pre_commit.languages import lua from pre_commit.languages import node from pre_commit.languages import perl @@ -33,6 +34,7 @@ 'fail': fail, 'golang': golang, 'haskell': haskell, + 'julia': julia, 'lua': lua, 'node': node, 'perl': perl, diff --git a/pre_commit/languages/julia.py b/pre_commit/languages/julia.py new file mode 100644 index 000000000..f565fe87e --- /dev/null +++ b/pre_commit/languages/julia.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import contextlib +from collections.abc import Generator +from collections.abc import Sequence + +from pre_commit import lang_base +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.prefix import Prefix +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'juliaenv' +health_check = lang_base.basic_health_check +get_default_version = lang_base.basic_get_default_version + + +def run_hook( + prefix: Prefix, + entry: str, + args: Sequence[str], + file_args: Sequence[str], + *, + is_local: bool, + require_serial: bool, + color: bool, +) -> tuple[int, bytes]: + # `entry` is a (hook-repo relative) file followed by (optional) args, e.g. + # `bin/id.jl` or `bin/hook.jl --arg1 --arg2` so we + # 1) shell parse it and join with args with hook_cmd + # 2) prepend the hooks prefix path to the first argument (the file) + # 3) prepend `julia` as the interpreter + cmd = lang_base.hook_cmd(entry, args) + cmd = ('julia', prefix.path(cmd[0]), *cmd[1:]) + return lang_base.run_xargs( + cmd, + file_args, + require_serial=require_serial, + color=color, + ) + + +def get_env_patch(target_dir: str, version: str) -> PatchesT: + return ( + # Single entry pointing to the hook env + ('JULIA_LOAD_PATH', target_dir), + # May be set, remove it to not interfer with LOAD_PATH + ('JULIA_PROJECT', ''), + ) + + +@contextlib.contextmanager +def in_env(prefix: Prefix, version: str) -> Generator[None]: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + with envcontext(get_env_patch(envdir, version)): + yield + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) + with contextlib.ExitStack() as ctx: + ctx.enter_context(in_env(prefix, version)) + + # TODO: Support language_version with juliaup similar to rust via + # rustup + # if version != 'system': + # ... + + joined_deps = '' + if len(additional_dependencies) > 0: + # Pkg.REPLMode.pkgstr + joined_deps = ' '.join(additional_dependencies) + + # Julia code to setup and instantiate the hook environment + # TODO: This would be easier to read and work with if it can be put in + # a .jl file instead. + julia_code = f""" + hook_env = "{envdir}" + mkdir(hook_env) + # Copy Project.toml to hook env + project_names = ("JuliaProject.toml", "Project.toml") + project_found = false + for project_name in project_names + isfile(project_name) || continue + cp(project_name, joinpath(hook_env, project_name)) + global project_found = true + break + end + if !project_found + error("No (Julia)Project.toml found in hooks repository") + end + + # Copy Manifest.toml to hook env (not mandatory) + manifest_names = ("JuliaManifest.toml", "Manifest.toml") + for manifest_name in manifest_names + isfile(manifest_name) || continue + cp(manifest_name, joinpath(hook_env, manifest_name)) + break + end + + # We prepend @stdlib here so that we can load the package manager even + # though `get_env_patch` limits `JULIA_LOAD_PATH` to just the hook env. + pushfirst!(LOAD_PATH, "@stdlib") + using Pkg + popfirst!(LOAD_PATH) + + # Template in any additional_dependencies + joined_deps = "{joined_deps}" + + # Instantiate the environment shipped with the hook repo. If we have + # additional dependencies we disable precompilation in this step to + # avoid double work. + precompile = isempty(joined_deps) ? "1" : "0" + withenv("JULIA_PKG_PRECOMPILE_AUTO" => precompile) do + Pkg.instantiate() + end + + # Add additional_dependencies (with precompilation) + if !isempty(joined_deps) + withenv("JULIA_PKG_PRECOMPILE_AUTO" => "1") do + Pkg.REPLMode.pkgstr("add " * joined_deps) + end + end + """ + cmd_output_b( + 'julia', '-e', julia_code, + cwd=prefix.prefix_dir, + )