Skip to content

Commit

Permalink
Make Go a first class language
Browse files Browse the repository at this point in the history
  • Loading branch information
taoufik07 authored and asottile committed Jan 13, 2023
1 parent ceb429b commit 9afd639
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 13 deletions.
116 changes: 108 additions & 8 deletions pre_commit/languages/golang.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,20 +29,100 @@
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


Expand All @@ -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:
Expand All @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@ package main

import (
"fmt"
"runtime"
"github.com/BurntSushi/toml"
"os"
)

type Config struct {
What string
}

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)
}
43 changes: 43 additions & 0 deletions tests/languages/golang_test.py
Original file line number Diff line number Diff line change
@@ -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)
27 changes: 23 additions & 4 deletions tests/repository_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) == []


Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 9afd639

Please sign in to comment.