diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 4db3bc3..e34646d 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -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] diff --git a/docs/pre-commit.md b/docs/pre-commit.md index a1aaf9a..833849c 100644 --- a/docs/pre-commit.md +++ b/docs/pre-commit.md @@ -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. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..cada6b3 --- /dev/null +++ b/setup.py @@ -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)