Skip to content

Commit 9afd639

Browse files
taoufik07asottile
authored andcommitted
Make Go a first class language
1 parent ceb429b commit 9afd639

File tree

4 files changed

+181
-13
lines changed

4 files changed

+181
-13
lines changed

pre_commit/languages/golang.py

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
from __future__ import annotations
22

33
import contextlib
4+
import functools
5+
import json
46
import os.path
7+
import platform
8+
import shutil
59
import sys
10+
import tarfile
11+
import tempfile
12+
import urllib.error
13+
import urllib.request
14+
import zipfile
15+
from typing import ContextManager
616
from typing import Generator
17+
from typing import IO
18+
from typing import Protocol
719
from typing import Sequence
820

921
import pre_commit.constants as C
@@ -17,20 +29,100 @@
1729
from pre_commit.util import rmtree
1830

1931
ENVIRONMENT_DIR = 'golangenv'
20-
get_default_version = helpers.basic_get_default_version
2132
health_check = helpers.basic_health_check
2233

34+
_ARCH_ALIASES = {
35+
'x86_64': 'amd64',
36+
'i386': '386',
37+
'aarch64': 'arm64',
38+
'armv8': 'arm64',
39+
'armv7l': 'armv6l',
40+
}
41+
_ARCH = platform.machine().lower()
42+
_ARCH = _ARCH_ALIASES.get(_ARCH, _ARCH)
43+
44+
45+
class ExtractAll(Protocol):
46+
def extractall(self, path: str) -> None: ...
47+
48+
49+
if sys.platform == 'win32': # pragma: win32 cover
50+
_EXT = 'zip'
51+
52+
def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]:
53+
return zipfile.ZipFile(bio)
54+
else: # pragma: win32 no cover
55+
_EXT = 'tar.gz'
56+
57+
def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]:
58+
return tarfile.open(fileobj=bio)
59+
60+
61+
@functools.lru_cache(maxsize=1)
62+
def get_default_version() -> str:
63+
if helpers.exe_exists('go'):
64+
return 'system'
65+
else:
66+
return C.DEFAULT
67+
68+
69+
def get_env_patch(venv: str, version: str) -> PatchesT:
70+
if version == 'system':
71+
return (
72+
('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
73+
)
2374

24-
def get_env_patch(venv: str) -> PatchesT:
2575
return (
26-
('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
76+
('GOROOT', os.path.join(venv, '.go')),
77+
(
78+
'PATH', (
79+
os.path.join(venv, 'bin'), os.pathsep,
80+
os.path.join(venv, '.go', 'bin'), os.pathsep, Var('PATH'),
81+
),
82+
),
2783
)
2884

2985

86+
@functools.lru_cache
87+
def _infer_go_version(version: str) -> str:
88+
if version != C.DEFAULT:
89+
return version
90+
resp = urllib.request.urlopen('https://go.dev/dl/?mode=json')
91+
# TODO: 3.9+ .removeprefix('go')
92+
return json.load(resp)[0]['version'][2:]
93+
94+
95+
def _get_url(version: str) -> str:
96+
os_name = platform.system().lower()
97+
version = _infer_go_version(version)
98+
return f'https://dl.google.com/go/go{version}.{os_name}-{_ARCH}.{_EXT}'
99+
100+
101+
def _install_go(version: str, dest: str) -> None:
102+
try:
103+
resp = urllib.request.urlopen(_get_url(version))
104+
except urllib.error.HTTPError as e: # pragma: no cover
105+
if e.code == 404:
106+
raise ValueError(
107+
f'Could not find a version matching your system requirements '
108+
f'(os={platform.system().lower()}; arch={_ARCH})',
109+
) from e
110+
else:
111+
raise
112+
else:
113+
with tempfile.TemporaryFile() as f:
114+
shutil.copyfileobj(resp, f)
115+
f.seek(0)
116+
117+
with _open_archive(f) as archive:
118+
archive.extractall(dest)
119+
shutil.move(os.path.join(dest, 'go'), os.path.join(dest, '.go'))
120+
121+
30122
@contextlib.contextmanager
31-
def in_env(prefix: Prefix) -> Generator[None, None, None]:
32-
envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, C.DEFAULT)
33-
with envcontext(get_env_patch(envdir)):
123+
def in_env(prefix: Prefix, version: str) -> Generator[None, None, None]:
124+
envdir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version)
125+
with envcontext(get_env_patch(envdir, version)):
34126
yield
35127

36128

@@ -39,15 +131,23 @@ def install_environment(
39131
version: str,
40132
additional_dependencies: Sequence[str],
41133
) -> None:
42-
helpers.assert_version_default('golang', version)
43134
env_dir = helpers.environment_dir(prefix, ENVIRONMENT_DIR, version)
44135

136+
if version != 'system':
137+
_install_go(version, env_dir)
138+
45139
if sys.platform == 'cygwin': # pragma: no cover
46140
gopath = cmd_output('cygpath', '-w', env_dir)[1].strip()
47141
else:
48142
gopath = env_dir
143+
49144
env = dict(os.environ, GOPATH=gopath)
50145
env.pop('GOBIN', None)
146+
if version != 'system':
147+
env['GOROOT'] = os.path.join(env_dir, '.go')
148+
env['PATH'] = os.pathsep.join((
149+
os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'],
150+
))
51151

52152
helpers.run_setup_cmd(prefix, ('go', 'install', './...'), env=env)
53153
for dependency in additional_dependencies:
@@ -64,5 +164,5 @@ def run_hook(
64164
file_args: Sequence[str],
65165
color: bool,
66166
) -> tuple[int, bytes]:
67-
with in_env(hook.prefix):
167+
with in_env(hook.prefix, hook.language_version):
68168
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

testing/resources/golang_hooks_repo/golang-hello-world/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@ package main
33

44
import (
55
"fmt"
6+
"runtime"
67
"github.com/BurntSushi/toml"
8+
"os"
79
)
810

911
type Config struct {
1012
What string
1113
}
1214

1315
func main() {
16+
message := runtime.Version()
17+
if len(os.Args) > 1 {
18+
message = os.Args[1]
19+
}
1420
var conf Config
1521
toml.Decode("What = 'world'\n", &conf)
16-
fmt.Printf("hello %v\n", conf.What)
22+
fmt.Printf("hello %v from %s\n", conf.What, message)
1723
}

tests/languages/golang_test.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from unittest import mock
5+
6+
import pytest
7+
8+
import pre_commit.constants as C
9+
from pre_commit.languages import golang
10+
from pre_commit.languages import helpers
11+
12+
13+
ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__
14+
15+
16+
@pytest.fixture
17+
def exe_exists_mck():
18+
with mock.patch.object(helpers, 'exe_exists') as mck:
19+
yield mck
20+
21+
22+
def test_golang_default_version_system_available(exe_exists_mck):
23+
exe_exists_mck.return_value = True
24+
assert ACTUAL_GET_DEFAULT_VERSION() == 'system'
25+
26+
27+
def test_golang_default_version_system_not_available(exe_exists_mck):
28+
exe_exists_mck.return_value = False
29+
assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT
30+
31+
32+
ACTUAL_INFER_GO_VERSION = golang._infer_go_version.__wrapped__
33+
34+
35+
def test_golang_infer_go_version_not_default():
36+
assert ACTUAL_INFER_GO_VERSION('1.19.4') == '1.19.4'
37+
38+
39+
def test_golang_infer_go_version_default():
40+
version = ACTUAL_INFER_GO_VERSION(C.DEFAULT)
41+
42+
assert version != C.DEFAULT
43+
assert re.match(r'^\d+\.\d+\.\d+$', version)

tests/repository_test.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -380,17 +380,36 @@ def test_swift_hook(tempdir_factory, store):
380380
)
381381

382382

383-
def test_golang_hook(tempdir_factory, store):
383+
def test_golang_system_hook(tempdir_factory, store):
384384
_test_hook_repo(
385385
tempdir_factory, store, 'golang_hooks_repo',
386-
'golang-hook', [], b'hello world\n',
386+
'golang-hook', ['system'], b'hello world from system\n',
387+
config_kwargs={
388+
'hooks': [{
389+
'id': 'golang-hook',
390+
'language_version': 'system',
391+
}],
392+
},
393+
)
394+
395+
396+
def test_golang_versioned_hook(tempdir_factory, store):
397+
_test_hook_repo(
398+
tempdir_factory, store, 'golang_hooks_repo',
399+
'golang-hook', [], b'hello world from go1.18.4\n',
400+
config_kwargs={
401+
'hooks': [{
402+
'id': 'golang-hook',
403+
'language_version': '1.18.4',
404+
}],
405+
},
387406
)
388407

389408

390409
def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store):
391410
gobin_dir = tempdir_factory.get()
392411
with envcontext((('GOBIN', gobin_dir),)):
393-
test_golang_hook(tempdir_factory, store)
412+
test_golang_system_hook(tempdir_factory, store)
394413
assert os.listdir(gobin_dir) == []
395414

396415

@@ -677,7 +696,7 @@ def test_additional_golang_dependencies_installed(
677696
envdir = helpers.environment_dir(
678697
hook.prefix,
679698
golang.ENVIRONMENT_DIR,
680-
C.DEFAULT,
699+
golang.get_default_version(),
681700
)
682701
binaries = os.listdir(os.path.join(envdir, 'bin'))
683702
# normalize for windows

0 commit comments

Comments
 (0)