diff --git a/.craft.yml b/.craft.yml index 0b9c80d94a..b0fa563979 100644 --- a/.craft.yml +++ b/.craft.yml @@ -95,6 +95,9 @@ targets: checksums: - algorithm: sha256 format: hex + - name: pypi + - name: sentry-pypi + internalPypiRepo: getsentry/pypi - name: docker id: Docker Hub (release) source: ghcr.io/getsentry/sentry-cli @@ -115,3 +118,5 @@ requireNames: - /^sentry-cli-Windows-i686.exe$/ - /^sentry-cli-Windows-x86_64.exe$/ - /^sentry-cli-Windows-aarch64.exe$/ + - /^sentry_cli-.*.tar.gz$/ + - /^sentry_cli-.*.whl$/ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1408612c8d..0737cf0283 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -248,6 +248,49 @@ jobs: path: '*.tgz' if-no-files-found: 'error' + python-base: + name: python (base) + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - name: Add Rustup Target + run: rustup target add x86_64-unknown-linux-musl + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 5.6.0 + with: + python-version: '3.11' + - run: python3 -m pip install build && python3 -m build + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + with: + name: python-base + path: dist/* + if-no-files-found: 'error' + + python: + name: python + runs-on: ubuntu-24.04 + needs: [linux, sign-macos-binaries, windows, python-base] + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 5.6.0 + with: + python-version: '3.11' + - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # 5.0.0 + with: + pattern: artifact-bin-* + merge-multiple: true + path: binaries + - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # 5.0.0 + with: + name: python-base + merge-multiple: true + path: python-base + - run: scripts/wheels --binaries binaries --base python-base --dest dist + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + with: + name: artifact-pkg-python + path: dist/* + if-no-files-found: 'error' + npm-distributions: name: 'Build NPM distributions' runs-on: ubuntu-24.04 @@ -350,7 +393,7 @@ jobs: merge: name: Create Release Artifact runs-on: ubuntu-24.04 - needs: [linux, sign-macos-binaries, windows, npm-distributions, node] + needs: [linux, sign-macos-binaries, windows, npm-distributions, node, python] steps: - uses: actions/upload-artifact/merge@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 with: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..31ffe0486b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[build-system] +requires = ["setuptools", "wheel", "setuptools-rust"] diff --git a/scripts/wheels b/scripts/wheels new file mode 100755 index 0000000000..7219f6705d --- /dev/null +++ b/scripts/wheels @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +import argparse +import base64 +import hashlib +import os.path +import shutil +import tempfile +import zipfile +from typing import NamedTuple + + +class Wheel(NamedTuple): + src: str + plat: str + exe: str = 'sentry-cli' + + +WHEELS = ( + Wheel( + src='sentry-cli-Darwin-arm64', + plat='macosx_11_0_arm64', + ), + Wheel( + src='sentry-cli-Darwin-universal', + plat='macosx_11_0_universal2', + ), + Wheel( + src='sentry-cli-Darwin-x86_64', + plat='macosx_10_15_x86_64', + ), + Wheel( + src='sentry-cli-Linux-aarch64', + plat='manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_2_aarch64', + ), + Wheel( + src='sentry-cli-Linux-armv7', + plat='manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_2_armv7l', + ), + Wheel( + src='sentry-cli-Linux-i686', + plat='manylinux_2_17_i686.manylinux2014_i686.musllinux_1_2_i686', + ), + Wheel( + src='sentry-cli-Linux-x86_64', + plat='manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_2_x86_64', + ), + Wheel( + src='sentry-cli-Windows-i686.exe', + plat='win32', + exe='sentry-cli.exe', + ), + Wheel( + src='sentry-cli-Windows-x86_64.exe', + plat='win_amd64', + exe='sentry-cli.exe', + ), + Wheel( + src='sentry-cli-Windows-aarch64.exe', + plat='win_arm64', + exe='sentry-cli.exe', + ), +) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('--binaries', required=True) + parser.add_argument('--base', required=True) + parser.add_argument('--dest', required=True) + args = parser.parse_args() + + expected = {wheel.src for wheel in WHEELS} + received = set(os.listdir(args.binaries)) + if expected < received: + raise SystemExit( + f'Unexpected binaries:\n\n' + f'- extra: {", ".join(sorted(received - expected))}\n' + f'- missing: {", ".join(sorted(expected - received))}' + ) + + sdist_path = wheel_path = None + for fname in os.listdir(args.base): + if fname.endswith('.tar.gz'): + sdist_path = os.path.join(args.base, fname) + elif fname.endswith('.whl'): + wheel_path = os.path.join(args.base, fname) + else: + raise SystemExit(f'unexpected file in `--base`: {fname}') + + if sdist_path is None or wheel_path is None: + raise SystemExit('expected wheel and sdist in `--base`') + + os.makedirs(args.dest, exist_ok=True) + shutil.copy(sdist_path, args.dest) + + for wheel in WHEELS: + binary_src = os.path.join(args.binaries, wheel.src) + binary_size = os.stat(binary_src).st_size + with open(binary_src, 'rb') as bf: + digest = hashlib.sha256(bf.read()).digest() + digest_b64 = base64.urlsafe_b64encode(digest).rstrip(b'=').decode() + + basename = os.path.basename(wheel_path) + wheelname, _ = os.path.splitext(basename) + name, version, py, abi, plat = wheelname.split('-') + + with tempfile.TemporaryDirectory() as tmp: + with zipfile.ZipFile(wheel_path) as zipf: + zipf.extractall(tmp) + + distinfo = os.path.join(tmp, f'{name}-{version}.dist-info') + scripts = os.path.join(tmp, f'{name}-{version}.data', 'scripts') + + # replace the script binary with our copy + os.remove(os.path.join(scripts, 'sentry-cli')) + shutil.copy(binary_src, os.path.join(scripts, wheel.exe)) + + # rewrite RECORD to include the new file + record_fname = os.path.join(distinfo, 'RECORD') + with open(record_fname) as f: + record_lines = list(f) + + record = f'{name}-{version}.data/scripts/sentry-cli,' + for i, line in enumerate(record_lines): + if line.startswith(record): + record_lines[i] = ( + f'{name}-{version}.data/scripts/{wheel.exe},' + f'sha256={digest_b64},' + f'{binary_size}\n' + ) + break + else: + raise SystemExit(f'could not find {record!r} in RECORD') + + with open(record_fname, 'w') as f: + f.writelines(record_lines) + + # rewrite WHEEL to have the new tags + wheel_fname = os.path.join(distinfo, 'WHEEL') + with open(wheel_fname) as f: + wheel_lines = list(f) + + for i, line in enumerate(wheel_lines): + if line.startswith('Tag: '): + wheel_lines[i:i + 1] = [ + f'Tag: {py}-{abi}-{plat}\n' + for plat in wheel.plat.split('.') + ] + break + else: + raise SystemExit("could not find 'Tag: ' in WHEEL") + + with open(wheel_fname, 'w') as f: + f.writelines(wheel_lines) + + # write out the final zip + new_basename = f'{name}-{version}-{py}-{abi}-{wheel.plat}.whl' + tmp_new_wheel = os.path.join(tmp, new_basename) + fnames = sorted( + os.path.join(root, fname) + for root, _, fnames in os.walk(tmp) + for fname in fnames + ) + with zipfile.ZipFile(tmp_new_wheel, 'w') as zipf: + for fname in fnames: + zinfo = zipfile.ZipInfo(os.path.relpath(fname, tmp)) + if '/scripts/' in zinfo.filename: + zinfo.external_attr = 0o100755 << 16 + with open(fname, 'rb') as fb: + zipf.writestr(zinfo, fb.read()) + + # move into dest + shutil.move(tmp_new_wheel, args.dest) + + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000..38bad4ae47 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,21 @@ +[metadata] +name = sentry_cli +description = A command line utility to work with Sentry. +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/getsentry/sentry-cli +author = Sentry +author_email = oss@sentry.io +license = BSD-3-Clause +license_file = LICENSE +classifiers = + License :: OSI Approved :: BSD License + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + +[options] +packages = +py_modules = +python_requires = >=3.7 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000..8f8f905b14 --- /dev/null +++ b/setup.py @@ -0,0 +1,26 @@ +from setuptools import setup +from setuptools_rust import RustBin +from wheel.bdist_wheel import bdist_wheel as _bdist_wheel + +with open('Cargo.toml') as f: + for line in f: + if line.startswith('version = "'): + _, VERSION, _ = line.split('"') + break + + +class bdist_wheel(_bdist_wheel): + def finalize_options(self): + super().finalize_options() + self.root_is_pure = False + + def get_tag(self): + _, _, plat = super().get_tag() + return 'py3', 'none', plat + + +setup( + version=VERSION, + rust_extensions=[RustBin("sentry-cli")], + cmdclass={'bdist_wheel': bdist_wheel}, +)