diff --git a/pre_commit/languages/lua.py b/pre_commit/languages/lua.py index 6e943f2af..3b599f816 100644 --- a/pre_commit/languages/lua.py +++ b/pre_commit/languages/lua.py @@ -5,6 +5,7 @@ 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 @@ -20,43 +21,51 @@ healthy = helpers.basic_healthy -def _find_lua() -> str: # pragma: win32 no cover +def _find_lua(language_version: str) -> str: # pragma: win32 no cover """Find a lua executable. Lua doesn't always have a plain `lua` executable. Some OS vendors will ship the binary as `lua#.#` (e.g., lua5.3) so discovery is needed to find a valid executable. """ - choices = ['lua'] - for path in os.environ.get('PATH', '').split(os.pathsep): - try: - candidates = os.listdir(path) - except OSError: - # Invalid path on PATH or lacking permissions. - continue - - for candidate in candidates: - # The Lua executable might look like `lua#.#` or `lua-#.#`. - if re.search(r'^lua[-]?\d+\.\d+', candidate): - choices.append(candidate) - - found_exes = [ - exe for exe in choices - if find_executable(exe) - ] + if language_version == C.DEFAULT: + choices = ['lua'] + for path in os.environ.get('PATH', '').split(os.pathsep): + try: + candidates = os.listdir(path) + except OSError: + # Invalid path on PATH or lacking permissions. + continue + + for candidate in candidates: + # The Lua executable might look like `lua#.#` or `lua-#.#`. + if re.search(r'^lua[-]?\d+\.\d+', candidate): + choices.append(candidate) + else: + # Prefer version specific executables first if available. + # This should avoid the corner case where a user requests a language + # version, gets a `lua` executable, but that executable is actually + # for a different version and package.path would patch LUA_PATH + # incorrectly. + choices = [f'lua{language_version}', 'lua-{language_version}', 'lua'] + + found_exes = [exe for exe in choices if find_executable(exe)] if found_exes: return found_exes[0] - raise ValueError('No lua executable found on the system paths.') + raise ValueError( + 'No lua executable found on the system paths ' + f'for {language_version} version.', + ) -def _get_lua_path_version() -> str: # pragma: win32 no cover +def _get_lua_path_version( + lua_executable: str, +) -> str: # pragma: win32 no cover """Get the Lua version used in file paths.""" - lua = _find_lua() - # This could sniff out from _VERSION, but checking package.path should # provide an answer for *exactly* where lua is looking for packages. - _, stdout, _ = cmd_output(lua, '-e', 'print(package.path)') + _, stdout, _ = cmd_output(lua_executable, '-e', 'print(package.path)') match = re.search(fr'{os.sep}lua{os.sep}(.*?){os.sep}', stdout) if match: return match[1] @@ -64,8 +73,11 @@ def _get_lua_path_version() -> str: # pragma: win32 no cover raise ValueError('Cannot determine lua version for file paths.') -def get_env_patch(env: str) -> PatchesT: # pragma: win32 no cover - version = _get_lua_path_version() +def get_env_patch( + env: str, language_version: str, +) -> PatchesT: # pragma: win32 no cover + lua = _find_lua(language_version) + version = _get_lua_path_version(lua) return ( ('PATH', (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))), ( @@ -92,7 +104,11 @@ def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: - with envcontext(get_env_patch(_envdir(prefix, language_version))): + with envcontext( + get_env_patch( + _envdir(prefix, language_version), language_version, + ), + ): yield diff --git a/tests/languages/lua_test.py b/tests/languages/lua_test.py index d6a4eb9b8..fba23b22f 100644 --- a/tests/languages/lua_test.py +++ b/tests/languages/lua_test.py @@ -3,6 +3,7 @@ import pytest +import pre_commit.constants as C from pre_commit.languages import lua from testing.util import xfailif_windows @@ -15,10 +16,19 @@ def test_find_lua(tmp_path, lua_name): lua_file = tmp_path / lua_name lua_file.touch(0o555) with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): - lua_executable = lua._find_lua() + lua_executable = lua._find_lua(C.DEFAULT) assert lua_name in lua_executable +def test_find_lua_language_version(tmp_path): + """Language discovery can find a specific version.""" + lua_file = tmp_path / 'lua5.99' + lua_file.touch(0o555) + with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): + lua_executable = lua._find_lua('5.99') + assert 'lua5.99' in lua_executable + + @pytest.mark.parametrize( ('invalid', 'mode'), ( @@ -34,7 +44,7 @@ def test_find_lua_fail(tmp_path, invalid, mode): non_lua_file.touch(mode) with mock.patch.dict(os.environ, {'PATH': str(tmp_path)}): with pytest.raises(ValueError): - lua._find_lua() + lua._find_lua(C.DEFAULT) @mock.patch.object(lua, 'cmd_output') @@ -42,4 +52,4 @@ def test_bad_package_path(mock_cmd_output): """A package path missing path info returns an unknown version.""" mock_cmd_output.return_value = (0, '', '') with pytest.raises(ValueError): - lua._get_lua_path_version() + lua._get_lua_path_version('lua') diff --git a/tests/repository_test.py b/tests/repository_test.py index 9650464c6..5f5e17e55 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1142,7 +1142,7 @@ def test_lua_hook(tempdir_factory, store): @skipif_cant_run_lua # pragma: win32 no cover def test_local_lua_additional_dependencies(store): - lua_entry = lua._find_lua() + lua_entry = lua._find_lua(C.DEFAULT) config = { 'repo': 'local', 'hooks': [{