[build] Harden build/release workflows (#16358)

Authored by: bashonly, Grub4K

Co-authored-by: Simon Sawicki <contact@grub4k.dev>
This commit is contained in:
bashonly
2026-03-27 19:10:58 -05:00
committed by GitHub
parent f01e1a1ced
commit 87eaf886f5
29 changed files with 5132 additions and 100 deletions

View File

@@ -0,0 +1,206 @@
#!/usr/bin/env python3
from __future__ import annotations
# Allow direct execution
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import contextlib
import dataclasses
import datetime as dt
import itertools
import json
import pathlib
import re
import urllib.request
from devscripts.utils import run_process
REQUIREMENTS_PATH = pathlib.Path(__file__).parent.parent / 'bundle/requirements'
INPUT_TMPL = 'requirements-{}.in'
OUTPUT_TMPL = 'requirements-{}.txt'
COOLDOWN_DATE = (dt.datetime.today() - dt.timedelta(days=5)).strftime('%Y-%m-%d')
CUSTOM_COMPILE_COMMAND = 'python -m devscripts.update_bundle_requirements'
LINUX_GNU_PYTHON_VERSION = '3.13'
LINUX_MUSL_PYTHON_VERISON = '3.14'
WINDOWS_INTEL_PYTHON_VERSION = '3.10'
WINDOWS_ARM64_PYTHON_VERSION = '3.13'
MACOS_PYTHON_VERSION = '3.14'
@dataclasses.dataclass
class Target:
platform: str
version: str
extras: list[str] = dataclasses.field(default_factory=list)
groups: list[str] = dataclasses.field(default_factory=list)
compile_args: list[str] = dataclasses.field(default_factory=list)
INSTALL_DEPS_TARGETS = {
'linux-x86_64': Target(
platform='x86_64-manylinux2014',
version=LINUX_GNU_PYTHON_VERSION,
extras=['default', 'curl-cffi-compat', 'secretstorage'],
groups=['pyinstaller'],
),
'linux-aarch64': Target(
platform='aarch64-manylinux2014',
version=LINUX_GNU_PYTHON_VERSION,
extras=['default', 'curl-cffi-compat', 'secretstorage'],
groups=['pyinstaller'],
),
'linux-armv7l': Target(
platform='linux',
version=LINUX_GNU_PYTHON_VERSION,
extras=['default', 'curl-cffi', 'secretstorage'],
groups=['pyinstaller'],
),
'musllinux-x86_64': Target(
platform='x86_64-unknown-linux-musl',
version=LINUX_MUSL_PYTHON_VERISON,
extras=['default', 'curl-cffi', 'secretstorage'],
groups=['pyinstaller'],
),
'musllinux-aarch64': Target(
platform='aarch64-unknown-linux-musl',
version=LINUX_MUSL_PYTHON_VERISON,
extras=['default', 'secretstorage'],
groups=['pyinstaller'],
),
'win-x64': Target(
platform='x86_64-pc-windows-msvc',
version=WINDOWS_INTEL_PYTHON_VERSION,
extras=['default', 'curl-cffi'],
),
'win-x86': Target(
platform='i686-pc-windows-msvc',
version=WINDOWS_INTEL_PYTHON_VERSION,
extras=['default'],
),
'win-arm64': Target(
platform='aarch64-pc-windows-msvc',
version=WINDOWS_ARM64_PYTHON_VERSION,
extras=['default', 'curl-cffi'],
),
'macos': Target(
platform='macos',
version=MACOS_PYTHON_VERSION,
extras=['default', 'curl-cffi-compat'],
# NB: Resolve delocate and PyInstaller together since they share dependencies
groups=['delocate', 'pyinstaller'],
# curl-cffi and cffi don't provide universal2 wheels, so only directly install their deps
# NB: uv's --no-emit-package option is equivalent to pip-compile's --unsafe-package option
compile_args=['--no-emit-package', 'curl-cffi', '--no-emit-package', 'cffi'],
),
# We fuse our own universal2 wheels for curl-cffi+cffi, so we need a separate requirements file
'macos-curl_cffi': Target(
platform='macos',
version=MACOS_PYTHON_VERSION,
extras=['curl-cffi-compat'],
# Only need curl-cffi+cffi in this requirements file; their deps are installed directly
compile_args=['--no-emit-package', 'certifi', '--no-emit-package', 'pycparser'],
),
}
@dataclasses.dataclass
class PyInstallerTarget:
platform: str
version: str
asset_tag: str
PYINSTALLER_BUILDS_TARGETS = {
'win-x64-pyinstaller': PyInstallerTarget(
platform='x86_64-pc-windows-msvc',
version=WINDOWS_INTEL_PYTHON_VERSION,
asset_tag='win_amd64',
),
'win-x86-pyinstaller': PyInstallerTarget(
platform='i686-pc-windows-msvc',
version=WINDOWS_INTEL_PYTHON_VERSION,
asset_tag='win32',
),
'win-arm64-pyinstaller': PyInstallerTarget(
platform='aarch64-pc-windows-msvc',
version=WINDOWS_ARM64_PYTHON_VERSION,
asset_tag='win_arm64',
),
}
PYINSTALLER_BUILDS_URL = 'https://api.github.com/repos/yt-dlp/Pyinstaller-Builds/releases/latest'
PYINSTALLER_BUILDS_TMPL = '''\
{}pyinstaller@{} \\
--hash={}
'''
PYINSTALLER_VERSION_RE = re.compile(r'pyinstaller-(?P<version>[0-9]+\.[0-9]+\.[0-9]+)-')
def write_requirements_input(filepath: pathlib.Path, *args: str) -> None:
filepath.write_text(run_process(
sys.executable, '-m', 'devscripts.install_deps',
'--omit-default', '--print', *args).stdout)
def run_pip_compile(python_platform: str, python_version: str, requirements_input_path: pathlib.Path, *args: str) -> str:
return run_process(
'uv', 'pip', 'compile',
'--upgrade',
f'--exclude-newer={COOLDOWN_DATE}',
f'--python-platform={python_platform}',
f'--python-version={python_version}',
'--generate-hashes',
'--no-strip-markers',
f'--custom-compile-command={CUSTOM_COMPILE_COMMAND}',
str(requirements_input_path),
*args)
def main():
with contextlib.closing(urllib.request.urlopen(PYINSTALLER_BUILDS_URL)) as resp:
info = json.load(resp)
for target_suffix, target in PYINSTALLER_BUILDS_TARGETS.items():
asset_info = next(asset for asset in info['assets'] if target.asset_tag in asset['name'])
pyinstaller_version = PYINSTALLER_VERSION_RE.match(asset_info['name']).group('version')
base_requirements_path = REQUIREMENTS_PATH / INPUT_TMPL.format(target_suffix)
base_requirements_path.write_text(f'pyinstaller=={pyinstaller_version}\n')
pyinstaller_builds_deps = run_pip_compile(
target.platform, target.version, base_requirements_path,
'--color=never', '--no-emit-package=pyinstaller').stdout
requirements_path = REQUIREMENTS_PATH / OUTPUT_TMPL.format(target_suffix)
requirements_path.write_text(PYINSTALLER_BUILDS_TMPL.format(
pyinstaller_builds_deps, asset_info['browser_download_url'], asset_info['digest']))
for target_suffix, target in INSTALL_DEPS_TARGETS.items():
requirements_input_path = REQUIREMENTS_PATH / INPUT_TMPL.format(target_suffix)
write_requirements_input(
requirements_input_path,
*itertools.chain.from_iterable(itertools.product(['--include-extra'], target.extras)),
*itertools.chain.from_iterable(itertools.product(['--include-group'], target.groups)))
run_pip_compile(
target.platform, target.version, requirements_input_path, *target.compile_args,
f'--output-file={REQUIREMENTS_PATH / OUTPUT_TMPL.format(target_suffix)}')
pypi_input_path = REQUIREMENTS_PATH / INPUT_TMPL.format('pypi-build')
write_requirements_input(pypi_input_path, '--include-group', 'build')
run_pip_compile(
'linux', LINUX_GNU_PYTHON_VERSION, pypi_input_path,
f'--output-file={REQUIREMENTS_PATH / OUTPUT_TMPL.format("pypi-build")}')
pip_input_path = REQUIREMENTS_PATH / INPUT_TMPL.format('pip')
write_requirements_input(pip_input_path, '--include-group', 'build', '--cherry-pick', 'pip')
run_pip_compile(
'windows', WINDOWS_INTEL_PYTHON_VERSION, pip_input_path,
f'--output-file={REQUIREMENTS_PATH / OUTPUT_TMPL.format("pip")}')
if __name__ == '__main__':
main()