diff --git a/changelog.d/+staticx-aarch64-wheel-tag.infrastructure.md b/changelog.d/+staticx-aarch64-wheel-tag.infrastructure.md new file mode 100644 index 00000000..5d47cf06 --- /dev/null +++ b/changelog.d/+staticx-aarch64-wheel-tag.infrastructure.md @@ -0,0 +1 @@ +Retag the staticx wheel built on Linux aarch64 as `manylinux2014_aarch64`, because uv 0.9.16 and newer reject its `manylinux1_aarch64` tag, which blocked building the aarch64 standalone executables. diff --git a/noxfile.py b/noxfile.py index 07991a07..0a1ba8cb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -17,9 +17,17 @@ import re import string import subprocess +import sys +import tempfile +import urllib.request import nox +if sys.version_info >= (3, 11): + import tomllib +else: # nox depends on tomli on these Python versions + import tomli as tomllib + UPSTREAM_REPO_URL = 'git@github.com:Backblaze/B2_Command_Line_Tool.git' CI = os.environ.get('CI') is not None @@ -64,6 +72,10 @@ def _detect_python_nox_id() -> str: SYSTEM = platform.system().lower() MACHINE = platform.machine().lower() +# staticx tags wheels built from its sdist as manylinux1, which does not exist for aarch64 (PEP 599), +# so uv refuses to install them. Keep in sync with the `wheel` marker in the `bundle` group in pyproject.toml. +RETAG_STATICX_WHEEL = SYSTEM == 'linux' and MACHINE == 'aarch64' + WINDOWS_TIMESTAMP_SERVER = 'http://timestamp.digicert.com' WINDOWS_SIGNTOOL_PATH = 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe' @@ -84,6 +96,7 @@ def uv_install( *, groups: tuple[str, ...] = (), extras: tuple[str, ...] = (), + no_install_packages: tuple[str, ...] = (), editable: bool = False, ) -> None: args = ['sync', '--locked'] @@ -95,6 +108,8 @@ def uv_install( for extra in extras: if extra: args.extend(['--extra', extra]) + for package in no_install_packages: + args.extend(['--no-install-package', package]) uv_env = getattr(session.virtualenv, 'location', os.getenv('VIRTUAL_ENV')) session.run_install('uv', *args, env={'UV_PROJECT_ENVIRONMENT': uv_env}) if INSTALL_SDK_FROM: @@ -107,6 +122,33 @@ def uv_install( ) +def get_locked_sdist( + package: str, + lock_file: pathlib.Path = pathlib.Path('./uv.lock'), +) -> tuple[str, str]: + """Return the URL and the `:` hash of the package's sdist in uv.lock.""" + lock = tomllib.loads(lock_file.read_text()) + entries = [entry for entry in lock['package'] if entry['name'] == package] + if len(entries) != 1: + raise RuntimeError(f'Expected one {package} entry in {lock_file}, found {len(entries)}.') + sdist = entries[0]['sdist'] + return sdist['url'], sdist['hash'] + + +def download_verified(url: str, expected_hash: str, directory: pathlib.Path) -> pathlib.Path: + """Download a file into the directory and fail unless it matches the `:` hash.""" + algorithm, expected_digest = expected_hash.split(':', maxsplit=1) + path = directory / url.rsplit('/', maxsplit=1)[-1] + with urllib.request.urlopen(url, timeout=60) as response: + path.write_bytes(response.read()) + [actual_hash] = _calculate_hashes(path, [algorithm]) + if actual_hash.hexdigest() != expected_digest: + raise RuntimeError( + f'{path.name} has {algorithm} digest {actual_hash.hexdigest()}, expected {expected_digest}.' + ) + return path + + def github_output(name, value, *, secret=False): gh_output_path = os.environ.get('GITHUB_OUTPUT') if secret: @@ -323,6 +365,43 @@ def dump_license(session: nox.Session): session.run('b2', 'license', '--dump', '--with-packages') +def install_retagged_staticx(session: nox.Session) -> None: + """Install staticx from its locked sdist, retagging the built wheel so that uv accepts it.""" + sdist_url, sdist_hash = get_locked_sdist('staticx') + with tempfile.TemporaryDirectory() as tmp_dir: + sdist = download_verified(sdist_url, sdist_hash, pathlib.Path(tmp_dir)) + wheel_dir = pathlib.Path(tmp_dir, 'wheels') + # Build without isolation, as `no-build-isolation-package` in pyproject.toml does for `uv sync`. + session.run_install( + 'uv', + 'build', + '--wheel', + '--no-build-isolation', + '--python', + session.virtualenv.location, + '--out-dir', + str(wheel_dir), + str(sdist), + ) + # Empty when `--no-install` skipped the build; `run_install` then skips retagging as well. + built_wheels = [str(path) for path in wheel_dir.glob('*.whl')] + session.run_install( + 'wheel', 'tags', '--remove', '--platform-tag', f'manylinux2014_{MACHINE}', *built_wheels + ) + session.run_install( + 'uv', + 'pip', + 'install', + '--no-deps', + '--no-index', + '--find-links', + str(wheel_dir), + '--python', + session.virtualenv.location, + 'staticx', + ) + + @nox.session(python=PYTHON_DEFAULT_VERSION) def bundle(session: nox.Session): """Bundle the distribution.""" @@ -337,7 +416,10 @@ def bundle(session: nox.Session): session, groups=('bundle',), extras=('full',), + no_install_packages=('staticx',) if RETAG_STATICX_WHEEL else (), ) + if RETAG_STATICX_WHEEL: + install_retagged_staticx(session) template_spec = string.Template(pathlib.Path('b2.spec.template').read_text()) versions = get_versions() @@ -730,8 +812,6 @@ def load_allowed_change_types( """ Load the list of allowed change types from the pyproject.toml file. """ - import tomllib - configuration = tomllib.loads(project_toml.read_text()) return set(entry['directory'] for entry in configuration['tool']['towncrier']['type']) diff --git a/pyproject.toml b/pyproject.toml index 79d875bc..eb09fe25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,7 @@ bundle = [ "patchelf-wrapper==1.2.0; platform_system == \"Linux\"", "staticx~=0.14.1; platform_system == \"Linux\"", "setuptools>=60,<80; platform_system == \"Linux\"", # required by non-isolated Linux bundle builds + "wheel~=0.48.0; platform_system == \"Linux\" and platform_machine == \"aarch64\"", # retags the staticx wheel, see noxfile.py ] [tool.uv] diff --git a/uv.lock b/uv.lock index ad7d6fb8..2495adbe 100644 --- a/uv.lock +++ b/uv.lock @@ -112,6 +112,7 @@ bundle = [ { name = "pyinstaller-hooks-contrib" }, { name = "setuptools", marker = "sys_platform == 'linux'" }, { name = "staticx", marker = "sys_platform == 'linux'" }, + { name = "wheel", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, ] doc = [ { name = "sadisplay" }, @@ -180,6 +181,7 @@ bundle = [ { name = "pyinstaller-hooks-contrib", specifier = ">=2023.6" }, { name = "setuptools", marker = "sys_platform == 'linux'", specifier = ">=60,<80" }, { name = "staticx", marker = "sys_platform == 'linux'", specifier = "~=0.14.1" }, + { name = "wheel", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'", specifier = "~=0.48.0" }, ] doc = [ { name = "sadisplay", specifier = ">=0.4.9" }, @@ -1977,3 +1979,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] + +[[package]] +name = "wheel" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/20/50ed6bdf27dec98b568a8ae25dc599f35baa3d9709f9e83fd1edb56b9a90/wheel-0.48.0.tar.gz", hash = "sha256:94800765601e9171bf5d58d066e640662842bcedcbab982b2c90787a2c987322", size = 66471, upload-time = "2026-08-11T22:02:27.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/29/69cfbb602cd91690c55d38ba9fe53e6a7e76a6fa647bf38f19c138d25449/wheel-0.48.0-py3-none-any.whl", hash = "sha256:3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab", size = 33320, upload-time = "2026-08-11T22:02:26.1Z" }, +]