Skip to content

Commit

Permalink
feat(pre-commit): Add binary based install
Browse files Browse the repository at this point in the history
For no additional build dependencies (besides Python >= 3.6), and faster
install.

The setup.py here is a copy of the one at
https://github.com/shellcheck-py/shellcheck-py with changes kept at the
bare minimum for future diffability.

We don't support checksumming at least yet, because there's no access to
them from cargo-release at the moment.

Closes #285
  • Loading branch information
scop committed Jun 28, 2021
1 parent e9c800b commit 867ee75
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 1 deletion.
10 changes: 9 additions & 1 deletion .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
- id: typos
name: typos
description: Source code spell checker
description: Source code spell checker, binary install
language: python
entry: typos
args: [--write-changes]
types: [text]

- id: typos-src
name: typos
description: Source code spell checker, source install
language: rust
entry: typos
args: [--write-changes]
Expand Down
4 changes: 4 additions & 0 deletions docs/pre-commit.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ repos:
- id: typos
```

The `typos` id installs a prebuilt executable from GitHub releases. If
one does not exist for the target platform, or if one built from
sources is preferred, use `typos-src` as the hook id instead.

Be sure to change `rev` to use the desired `typos` git tag or
revision.

Expand Down
166 changes: 166 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
import hashlib
import http
import io
import os.path
import stat
import sys
import tarfile
import urllib.request
import zipfile
from distutils.command.build import build as orig_build
from distutils.core import Command
from typing import Tuple

from setuptools import setup
from setuptools.command.install import install as orig_install

TYPOS_VERSION = '1.0.9'
POSTFIX_SHA256 = {
'linux': (
'x86_64-unknown-linux-gnu.tar.gz',
'', # TODO: sha256 hexhash when we can generate it with release
),
'darwin': (
'x86_64-apple-darwin.tar.gz',
'', # TODO: sha256 hexhash when we can generate it with release
),
'win32': (
'x86_64-pc-windows-msvc.zip',
'', # TODO: sha256 hexhash when we can generate it with release
),
}
PY_VERSION = '1'


def get_download_url() -> Tuple[str, str]:
postfix, sha256 = POSTFIX_SHA256[sys.platform]
url = (
f'https://github.com/crate-ci/typos/releases/download/'
f'v{TYPOS_VERSION}/typos-v{TYPOS_VERSION}-{postfix}'
)
return url, sha256


def download(url: str, sha256: str) -> bytes:
with urllib.request.urlopen(url) as resp:
code = resp.getcode()
if code != http.HTTPStatus.OK:
raise ValueError(f'HTTP failure. Code: {code}')
data = resp.read()

if not sha256:
return data

checksum = hashlib.sha256(data).hexdigest()
if checksum != sha256:
raise ValueError(f'sha256 mismatch, expected {sha256}, got {checksum}')

return data


def extract(url: str, data: bytes) -> bytes:
with io.BytesIO(data) as bio:
if '.tar.' in url:
with tarfile.open(fileobj=bio) as tarf:
for info in tarf.getmembers():
if info.isfile() and info.name.endswith('typos'):
return tarf.extractfile(info).read()
elif url.endswith('.zip'):
with zipfile.ZipFile(bio) as zipf:
for info in zipf.infolist():
if info.filename.endswith('.exe'):
return zipf.read(info.filename)

raise AssertionError(f'unreachable {url}')


def save_executable(data: bytes, base_dir: str):
exe = 'typos' if sys.platform != 'win32' else 'typos.exe'
output_path = os.path.join(base_dir, exe)
os.makedirs(base_dir)

with open(output_path, 'wb') as fp:
fp.write(data)

# Mark as executable.
# https://stackoverflow.com/a/14105527
mode = os.stat(output_path).st_mode
mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
os.chmod(output_path, mode)


class build(orig_build):
sub_commands = orig_build.sub_commands + [('fetch_binaries', None)]


class install(orig_install):
sub_commands = orig_install.sub_commands + [('install_typos', None)]


class fetch_binaries(Command):
build_temp = None

def initialize_options(self):
pass

def finalize_options(self):
self.set_undefined_options('build', ('build_temp', 'build_temp'))

def run(self):
# save binary to self.build_temp
url, sha256 = get_download_url()
archive = download(url, sha256)
data = extract(url, archive)
save_executable(data, self.build_temp)


class install_typos(Command):
description = 'install the typos executable'
outfiles = ()
build_dir = install_dir = None

def initialize_options(self):
pass

def finalize_options(self):
# this initializes attributes based on other commands' attributes
self.set_undefined_options('build', ('build_temp', 'build_dir'))
self.set_undefined_options(
'install', ('install_scripts', 'install_dir'),
)

def run(self):
self.outfiles = self.copy_tree(self.build_dir, self.install_dir)

def get_outputs(self):
return self.outfiles


command_overrides = {
'install': install,
'install_typos': install_typos,
'build': build,
'fetch_binaries': fetch_binaries,
}


try:
from wheel.bdist_wheel import bdist_wheel as orig_bdist_wheel
except ImportError:
pass
else:
class bdist_wheel(orig_bdist_wheel):
def finalize_options(self):
orig_bdist_wheel.finalize_options(self)
# Mark us as not a pure python package
self.root_is_pure = False

def get_tag(self):
_, _, plat = orig_bdist_wheel.get_tag(self)
# We don't contain any python source, nor any python extensions
return 'py2.py3', 'none', plat

command_overrides['bdist_wheel'] = bdist_wheel

setup(version=f'{TYPOS_VERSION}.{PY_VERSION}', cmdclass=command_overrides)

0 comments on commit 867ee75

Please sign in to comment.