Make Go a first class language

This commit is contained in:
taoufik07
2023-01-03 01:48:43 +01:00
committed by Anthony Sottile
parent ceb429b253
commit 9afd63948e
4 changed files with 181 additions and 13 deletions

View File

@@ -1,9 +1,21 @@
from __future__ import annotations
import contextlib
import functools
import json
import os.path
import platform
import shutil
import sys
import tarfile
import tempfile
import urllib.error
import urllib.request
import zipfile
from typing import ContextManager
from typing import Generator
from typing import IO
from typing import Protocol
from typing import Sequence
import pre_commit.constants as C
@@ -17,20 +29,100 @@ from pre_commit.util import cmd_output
from pre_commit.util import rmtree
ENVIRONMENT_DIR = 'golangenv'
get_default_version = helpers.basic_get_default_version
health_check = helpers.basic_health_check
_ARCH_ALIASES = {
'x86_64': 'amd64',
'i386': '386',
'aarch64': 'arm64',
'armv8': 'arm64',
'armv7l': 'armv6l',
}
_ARCH = platform.machine().lower()
_ARCH = _ARCH_ALIASES.get(_ARCH, _ARCH)
class ExtractAll(Protocol):
def extractall(self, path: str) -> None: ...
if sys.platform == 'win32': # pragma: win32 cover
_EXT = 'zip'
def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]:
return zipfile.ZipFile(bio)
else: # pragma: win32 no cover
_EXT = 'tar.gz'
def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]:
return tarfile.open(fileobj=bio)
@functools.lru_cache(maxsize=1)
def get_default_version() -> str:
if helpers.exe_exists('go'):
return 'system'
else:
return C.DEFAULT
def get_env_patch(venv: str, version: str) -> PatchesT:
if version == 'system':
return (
('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
)
def get_env_patch(venv: str) -> PatchesT:
return (
('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
('GOROOT', os.path.join(venv, '.go')),
(
'PATH', (
os.path.join(venv, 'bin'), os.pathsep,
os.path.join(venv, '.go', 'bin'), os.pathsep, Var('PATH'),
),
),
)
@functools.lru_cache
def _infer_go_version(version: str) -> str:
if version != C.DEFAULT:
return version
resp = urllib.request.urlopen('https://go.dev/dl/?mode=json')
# TODO: 3.9+ .removeprefix('go')
return json.load(resp)[0]['version'][2:]
def _get_url(version: str) -> str:
os_name = platform.system().lower()
version = _infer_go_version(version)
return f'https://dl.google.com/go/go{version}.{os_name}-{_ARCH}.{_EXT}'
def _install_go(version: str, dest: str) -> None:
try:
resp = urllib.request.urlopen(_get_url(version))
except urllib.error.HTTPError as e: # pragma: no cover
if e.code == 404:
raise ValueError(
f'Could not find a version matching your system requirements '
f'(os={platform.system().lower()}; arch={_ARCH})',
) from e
else:
raise
else:
with tempfile.TemporaryFile() as f:
shutil.copyfileobj(resp, f)
f.seek(0)
with _open_archive(f) as archive:
archive.extractall(dest)
shutil.move(os.path.join(dest, 'go'), os.path.join(dest, '.go'))
@contextlib.contextmanager
def in_env(prefix: Prefix) -> Generator[None, None, None]:
envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT)
with envcontext(get_env_patch(envdir)):
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version)
with envcontext(get_env_patch(envdir, version)):
yield
@@ -39,15 +131,23 @@ def install_environment(
version: str,
additional_dependencies: Sequence[str],
) -> None:
helpers.assert_version_default('golang', version)
env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version)
if version != 'system':
_install_go(version, env_dir)
if sys.platform == 'cygwin': # pragma: no cover
gopath = cmd_output('cygpath', '-w', env_dir)[1].strip()
else:
gopath = env_dir
env = dict(os.environ, GOPATH=gopath)
env.pop('GOBIN', None)
if version != 'system':
env['GOROOT'] = os.path.join(env_dir, '.go')
env['PATH'] = os.pathsep.join((
os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'],
))
helpers.run_setup_cmd(prefix, ('go', 'install', './...'), env=env)
for dependency in additional_dependencies:
@@ -64,5 +164,5 @@ def run_hook(
file_args: Sequence[str],
color: bool,
) -> tuple[int, bytes]:
with in_env(hook.prefix):
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

View File

@@ -3,7 +3,9 @@ package main
import (
"fmt"
"runtime"
"github.com/BurntSushi/toml"
"os"
)
type Config struct {
@@ -11,7 +13,11 @@ type Config struct {
}
func main() {
message := runtime.Version()
if len(os.Args) > 1 {
message = os.Args[1]
}
var conf Config
toml.Decode("What = 'world'\n", &conf)
fmt.Printf("hello %v\n", conf.What)
fmt.Printf("hello %v from %s\n", conf.What, message)
}

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
import re
from unittest import mock
import pytest
import pre_commit.constants as C
from pre_commit.languages import golang
from pre_commit.languages import helpers
ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__
@pytest.fixture
def exe_exists_mck():
with mock.patch.object(helpers, 'exe_exists') as mck:
yield mck
def test_golang_default_version_system_available(exe_exists_mck):
exe_exists_mck.return_value = True
assert ACTUAL_GET_DEFAULT_VERSION() == 'system'
def test_golang_default_version_system_not_available(exe_exists_mck):
exe_exists_mck.return_value = False
assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT
ACTUAL_INFER_GO_VERSION = golang._infer_go_version.__wrapped__
def test_golang_infer_go_version_not_default():
assert ACTUAL_INFER_GO_VERSION('1.19.4') == '1.19.4'
def test_golang_infer_go_version_default():
version = ACTUAL_INFER_GO_VERSION(C.DEFAULT)
assert version != C.DEFAULT
assert re.match(r'^\d+\.\d+\.\d+$', version)

View File

@@ -380,17 +380,36 @@ def test_swift_hook(tempdir_factory, store):
)
def test_golang_hook(tempdir_factory, store):
def test_golang_system_hook(tempdir_factory, store):
_test_hook_repo(
tempdir_factory, store, 'golang_hooks_repo',
'golang-hook', [], b'hello world\n',
'golang-hook', ['system'], b'hello world from system\n',
config_kwargs={
'hooks': [{
'id': 'golang-hook',
'language_version': 'system',
}],
},
)
def test_golang_versioned_hook(tempdir_factory, store):
_test_hook_repo(
tempdir_factory, store, 'golang_hooks_repo',
'golang-hook', [], b'hello world from go1.18.4\n',
config_kwargs={
'hooks': [{
'id': 'golang-hook',
'language_version': '1.18.4',
}],
},
)
def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store):
gobin_dir = tempdir_factory.get()
with envcontext((('GOBIN', gobin_dir),)):
test_golang_hook(tempdir_factory, store)
test_golang_system_hook(tempdir_factory, store)
assert os.listdir(gobin_dir) == []
@@ -677,7 +696,7 @@ def test_additional_golang_dependencies_installed(
envdir = helpers.environment_dir(
hook.prefix,
golang.ENVIRONMENT_DIR,
C.DEFAULT,
golang.get_default_version(),
)
binaries = os.listdir(os.path.join(envdir, 'bin'))
# normalize for windows