| 1 | #!/usr/bin/env python |
| 2 | |
| 3 | '''Fetch the MSYS2 installer.''' |
| 4 | |
| 5 | from __future__ import annotations |
| 6 | |
| 7 | import hashlib |
| 8 | import json |
| 9 | import shutil |
| 10 | import sys |
| 11 | |
| 12 | from pathlib import Path |
| 13 | from tempfile import TemporaryDirectory |
| 14 | from typing import Final |
| 15 | from urllib.request import Request, urlopen |
| 16 | |
| 17 | REPO: Final = 'msys2/msys2-installer' |
| 18 | |
| 19 | |
| 20 | def get_latest_release() -> tuple[str, str]: |
| 21 | '''Get the latest release for the repo.''' |
| 22 | REQUEST: Final = Request( |
| 23 | url=f'https://api.github.com/repos/{REPO}/releases', |
| 24 | headers={ |
| 25 | 'Accept': 'application/vnd.github+json', |
| 26 | 'X-GitHub-API-Version': '2022-11-28', |
| 27 | }, |
| 28 | method='GET', |
| 29 | ) |
| 30 | |
| 31 | print('>>> Fetching release list') |
| 32 | |
| 33 | with urlopen(REQUEST, timeout=15) as response: |
| 34 | if response.status != 200: |
| 35 | print(f'!!! Failed to fetch release list, status={response.status}') |
| 36 | sys.exit(1) |
| 37 | |
| 38 | data = json.load(response) |
| 39 | |
| 40 | data = list(filter(lambda x: x['name'] != 'Nightly Installer Build', data)) |
| 41 | |
| 42 | name = data[0]['name'] |
| 43 | version = data[0]['tag_name'].replace('-', '') |
| 44 | |
| 45 | return name, version |
| 46 | |
| 47 | |
| 48 | def fetch_release_asset(tmpdir: Path, name: str, file: str) -> Path: |
| 49 | '''Fetch a specific release asset.''' |
| 50 | REQUEST: Final = Request( |
| 51 | url=f'https://github.com/{REPO}/releases/download/{name}/{file}', |
| 52 | method='GET', |
| 53 | ) |
| 54 | TARGET: Final = tmpdir / file |
| 55 | |
| 56 | print(f'>>> Downloading {file}') |
| 57 | |
| 58 | with urlopen(REQUEST, timeout=15) as response: |
| 59 | if response.status != 200: |
| 60 | print(f'!!! Failed to fetch {file}, status={response.status}') |
| 61 | sys.exit(1) |
| 62 | |
| 63 | TARGET.write_bytes(response.read()) |
| 64 | |
| 65 | return TARGET |
| 66 | |
| 67 | |
| 68 | def main() -> None: |
| 69 | '''Core program logic.''' |
| 70 | if len(sys.argv) != 2: |
| 71 | print(f'{__file__} must be run with exactly one argument.') |
| 72 | |
| 73 | target = Path(sys.argv[1]) |
| 74 | tmp_target = target.with_name(f'.{target.name}.tmp') |
| 75 | |
| 76 | name, version = get_latest_release() |
| 77 | |
| 78 | with TemporaryDirectory() as tmpdir: |
| 79 | tmppath = Path(tmpdir) |
| 80 | |
| 81 | installer = fetch_release_asset(tmppath, name, f'msys2-base-x86_64-{version}.tar.zst') |
| 82 | checksums = fetch_release_asset(tmppath, name, f'msys2-base-x86_64-{version}.tar.zst.sha256') |
| 83 | |
| 84 | print('>>> Verifying SHA256 checksum') |
| 85 | expected_checksum = checksums.read_text().partition(' ')[0].casefold() |
| 86 | actual_checksum = hashlib.sha256(installer.read_bytes()).hexdigest().casefold() |
| 87 | |
| 88 | if expected_checksum != actual_checksum: |
| 89 | print('!!! Checksum mismatch') |
| 90 | print(f'!!! Expected: {expected_checksum}') |
| 91 | print(f'!!! Actual: {actual_checksum}') |
| 92 | sys.exit(1) |
| 93 | |
| 94 | print(f'>>> Copying to {target}') |
| 95 | |
| 96 | shutil.copy(installer, tmp_target) |
| 97 | tmp_target.replace(target) |
| 98 | |
| 99 | |
| 100 | if __name__ == '__main__': |
| 101 | main() |