|
| 1 | +import contextlib |
| 2 | +import os |
| 3 | +import re |
| 4 | +from typing import Generator |
| 5 | +from typing import Sequence |
| 6 | +from typing import Tuple |
| 7 | + |
| 8 | +import pre_commit.constants as C |
| 9 | +from pre_commit.envcontext import envcontext |
| 10 | +from pre_commit.envcontext import PatchesT |
| 11 | +from pre_commit.envcontext import Var |
| 12 | +from pre_commit.hook import Hook |
| 13 | +from pre_commit.languages import helpers |
| 14 | +from pre_commit.parse_shebang import find_executable |
| 15 | +from pre_commit.prefix import Prefix |
| 16 | +from pre_commit.util import clean_path_on_failure |
| 17 | +from pre_commit.util import cmd_output |
| 18 | + |
| 19 | +ENVIRONMENT_DIR = 'lua_env' |
| 20 | +get_default_version = helpers.basic_get_default_version |
| 21 | +healthy = helpers.basic_healthy |
| 22 | + |
| 23 | + |
| 24 | +def _find_lua(language_version: str) -> str: # pragma: win32 no cover |
| 25 | + """Find a lua executable. |
| 26 | +
|
| 27 | + Lua doesn't always have a plain `lua` executable. |
| 28 | + Some OS vendors will ship the binary as `lua#.#` (e.g., lua5.3) |
| 29 | + so discovery is needed to find a valid executable. |
| 30 | + """ |
| 31 | + if language_version == C.DEFAULT: |
| 32 | + choices = ['lua'] |
| 33 | + for path in os.environ.get('PATH', '').split(os.pathsep): |
| 34 | + try: |
| 35 | + candidates = os.listdir(path) |
| 36 | + except OSError: |
| 37 | + # Invalid path on PATH or lacking permissions. |
| 38 | + continue |
| 39 | + |
| 40 | + for candidate in candidates: |
| 41 | + # The Lua executable might look like `lua#.#` or `lua-#.#`. |
| 42 | + if re.search(r'^lua[-]?\d+\.\d+', candidate): |
| 43 | + choices.append(candidate) |
| 44 | + else: |
| 45 | + # Prefer version specific executables first if available. |
| 46 | + # This should avoid the corner case where a user requests a language |
| 47 | + # version, gets a `lua` executable, but that executable is actually |
| 48 | + # for a different version and package.path would patch LUA_PATH |
| 49 | + # incorrectly. |
| 50 | + choices = [f'lua{language_version}', 'lua-{language_version}', 'lua'] |
| 51 | + |
| 52 | + found_exes = [exe for exe in choices if find_executable(exe)] |
| 53 | + if found_exes: |
| 54 | + return found_exes[0] |
| 55 | + |
| 56 | + raise ValueError( |
| 57 | + 'No lua executable found on the system paths ' |
| 58 | + f'for {language_version} version.', |
| 59 | + ) |
| 60 | + |
| 61 | + |
| 62 | +def _get_lua_path_version( |
| 63 | + lua_executable: str, |
| 64 | +) -> str: # pragma: win32 no cover |
| 65 | + """Get the Lua version used in file paths.""" |
| 66 | + # This could sniff out from _VERSION, but checking package.path should |
| 67 | + # provide an answer for *exactly* where lua is looking for packages. |
| 68 | + _, stdout, _ = cmd_output(lua_executable, '-e', 'print(package.path)') |
| 69 | + sep = os.sep if os.name != 'nt' else os.sep * 2 |
| 70 | + match = re.search(fr'{sep}lua{sep}(.*?){sep}', stdout) |
| 71 | + if match: |
| 72 | + return match[1] |
| 73 | + |
| 74 | + raise ValueError('Cannot determine lua version for file paths.') |
| 75 | + |
| 76 | + |
| 77 | +def get_env_patch( |
| 78 | + env: str, language_version: str, |
| 79 | +) -> PatchesT: # pragma: win32 no cover |
| 80 | + lua = _find_lua(language_version) |
| 81 | + version = _get_lua_path_version(lua) |
| 82 | + return ( |
| 83 | + ('PATH', (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))), |
| 84 | + ( |
| 85 | + 'LUA_PATH', ( |
| 86 | + os.path.join(env, 'share', 'lua', version, '?.lua;'), |
| 87 | + os.path.join(env, 'share', 'lua', version, '?', 'init.lua;;'), |
| 88 | + ), |
| 89 | + ), |
| 90 | + ( |
| 91 | + 'LUA_CPATH', ( |
| 92 | + os.path.join(env, 'lib', 'lua', version, '?.so;;'), |
| 93 | + ), |
| 94 | + ), |
| 95 | + ) |
| 96 | + |
| 97 | + |
| 98 | +def _envdir(prefix: Prefix, version: str) -> str: # pragma: win32 no cover |
| 99 | + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) |
| 100 | + return prefix.path(directory) |
| 101 | + |
| 102 | + |
| 103 | +@contextlib.contextmanager # pragma: win32 no cover |
| 104 | +def in_env( |
| 105 | + prefix: Prefix, |
| 106 | + language_version: str, |
| 107 | +) -> Generator[None, None, None]: |
| 108 | + with envcontext( |
| 109 | + get_env_patch( |
| 110 | + _envdir(prefix, language_version), language_version, |
| 111 | + ), |
| 112 | + ): |
| 113 | + yield |
| 114 | + |
| 115 | + |
| 116 | +def install_environment( |
| 117 | + prefix: Prefix, |
| 118 | + version: str, |
| 119 | + additional_dependencies: Sequence[str], |
| 120 | +) -> None: # pragma: win32 no cover |
| 121 | + helpers.assert_version_default('lua', version) |
| 122 | + |
| 123 | + envdir = _envdir(prefix, version) |
| 124 | + with clean_path_on_failure(envdir): |
| 125 | + with in_env(prefix, version): |
| 126 | + # luarocks doesn't bootstrap a tree prior to installing |
| 127 | + # so ensure the directory exists. |
| 128 | + os.makedirs(envdir, exist_ok=True) |
| 129 | + |
| 130 | + make_cmd = ['luarocks', '--tree', envdir, 'make'] |
| 131 | + # Older luarocks (e.g., 2.4.2) expect the rockspec as an argument. |
| 132 | + filenames = prefix.star('.rockspec') |
| 133 | + make_cmd.extend(filenames[:1]) |
| 134 | + |
| 135 | + helpers.run_setup_cmd(prefix, tuple(make_cmd)) |
| 136 | + |
| 137 | + # luarocks can't install multiple packages at once |
| 138 | + # so install them individually. |
| 139 | + for dependency in additional_dependencies: |
| 140 | + cmd = ('luarocks', '--tree', envdir, 'install', dependency) |
| 141 | + helpers.run_setup_cmd(prefix, cmd) |
| 142 | + |
| 143 | + |
| 144 | +def run_hook( |
| 145 | + hook: Hook, |
| 146 | + file_args: Sequence[str], |
| 147 | + color: bool, |
| 148 | +) -> Tuple[int, bytes]: # pragma: win32 no cover |
| 149 | + with in_env(hook.prefix, hook.language_version): |
| 150 | + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) |
0 commit comments