name: Build Artifacts on: workflow_call: inputs: version: required: true type: string channel: required: false default: stable type: string origin: required: true type: string unix: default: true type: boolean linux: default: true type: boolean linux_armv7l: default: true type: boolean musllinux: default: true type: boolean macos: default: true type: boolean windows: default: true type: boolean secrets: GPG_SIGNING_KEY: required: false workflow_dispatch: inputs: version: description: | VERSION: yyyy.mm.dd[.rev] or rev (default: auto-generated) required: false default: '' type: string channel: description: | SOURCE of this build's updates: stable/nightly/master/ required: true default: stable type: string unix: description: yt-dlp, yt-dlp.tar.gz default: true type: boolean linux: description: yt-dlp_linux, yt-dlp_linux.zip, yt-dlp_linux_aarch64, yt-dlp_linux_aarch64.zip default: true type: boolean linux_armv7l: description: yt-dlp_linux_armv7l.zip default: true type: boolean musllinux: description: yt-dlp_musllinux, yt-dlp_musllinux.zip, yt-dlp_musllinux_aarch64, yt-dlp_musllinux_aarch64.zip default: true type: boolean macos: description: yt-dlp_macos, yt-dlp_macos.zip default: true type: boolean windows: description: yt-dlp.exe, yt-dlp_win.zip, yt-dlp_x86.exe, yt-dlp_win_x86.zip, yt-dlp_arm64.exe, yt-dlp_win_arm64.zip default: true type: boolean permissions: contents: read jobs: process: runs-on: ubuntu-latest outputs: origin: ${{ steps.process_inputs.outputs.origin }} timestamp: ${{ steps.process_inputs.outputs.timestamp }} version: ${{ steps.process_inputs.outputs.version }} linux_matrix: ${{ steps.linux_matrix.outputs.matrix }} steps: - name: Process inputs id: process_inputs env: INPUTS: ${{ toJSON(inputs) }} REPOSITORY: ${{ github.repository }} shell: python run: | import datetime as dt import json import os import re INPUTS = json.loads(os.environ['INPUTS']) timestamp = dt.datetime.now(tz=dt.timezone.utc).strftime('%Y.%m.%d.%H%M%S.%f') version = INPUTS.get('version') if version and '.' not in version: # build.yml was dispatched with only a revision as the version input value version_parts = [*timestamp.split('.')[:3], version] elif not version: # build.yml was dispatched without any version input value, so include .HHMMSS revision version_parts = timestamp.split('.')[:4] else: # build.yml was called or dispatched with a complete version input value version_parts = version.split('.') assert all(re.fullmatch(r'[0-9]+', part) for part in version_parts), 'Version must be numeric' outputs = { 'origin': INPUTS.get('origin') or os.environ['REPOSITORY'], 'timestamp': timestamp, 'version': '.'.join(version_parts), } print(json.dumps(outputs, indent=2)) with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write('\n'.join(f'{key}={value}' for key, value in outputs.items())) - name: Build Linux matrix id: linux_matrix env: INPUTS: ${{ toJSON(inputs) }} PYTHON_VERSION: '3.13' UPDATE_TO: yt-dlp/yt-dlp@2025.09.05 shell: python run: | import json import os EXE_MAP = { 'linux': [{ 'os': 'linux', 'arch': 'x86_64', 'runner': 'ubuntu-24.04', }, { 'os': 'linux', 'arch': 'aarch64', 'runner': 'ubuntu-24.04-arm', }], 'linux_armv7l': [{ 'os': 'linux', 'arch': 'armv7l', 'runner': 'ubuntu-24.04-arm', 'qemu_platform': 'linux/arm/v7', 'onefile': False, 'cache_requirements': True, 'update_to': 'yt-dlp/yt-dlp@2023.03.04', }], 'musllinux': [{ 'os': 'musllinux', 'arch': 'x86_64', 'runner': 'ubuntu-24.04', }, { 'os': 'musllinux', 'arch': 'aarch64', 'runner': 'ubuntu-24.04-arm', }], } INPUTS = json.loads(os.environ['INPUTS']) matrix = [exe for key, group in EXE_MAP.items() for exe in group if INPUTS.get(key)] if not matrix: # If we send an empty matrix when no linux inputs are given, the entire workflow fails matrix = [EXE_MAP['linux'][0]] for exe in matrix: exe['exe'] = '_'.join(filter(None, ( 'yt-dlp', exe['os'], exe['arch'] != 'x86_64' and exe['arch'], ))) exe.setdefault('qemu_platform', None) exe.setdefault('onefile', True) exe.setdefault('onedir', True) exe.setdefault('cache_requirements', False) exe.setdefault('python_version', os.environ['PYTHON_VERSION']) exe.setdefault('update_to', os.environ['UPDATE_TO']) if not any(INPUTS.get(key) for key in EXE_MAP): print('skipping linux job') else: print(json.dumps(matrix, indent=2)) with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write(f'matrix={json.dumps(matrix)}') unix: needs: process if: inputs.unix runs-on: ubuntu-latest env: CHANNEL: ${{ inputs.channel }} ORIGIN: ${{ needs.process.outputs.origin }} VERSION: ${{ needs.process.outputs.version }} UPDATE_TO: yt-dlp/yt-dlp@2025.09.05 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Needed for changelog - uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install Requirements run: | sudo apt -y install zip pandoc man sed - name: Prepare run: | python devscripts/update-version.py -c "${CHANNEL}" -r "${ORIGIN}" "${VERSION}" python devscripts/update_changelog.py -vv python devscripts/make_lazy_extractors.py - name: Build Unix platform-independent binary run: | make all tar - name: Verify --update-to if: vars.UPDATE_TO_VERIFICATION run: | chmod +x ./yt-dlp cp ./yt-dlp ./yt-dlp_downgraded version="$(./yt-dlp --version)" ./yt-dlp_downgraded -v --update-to "${UPDATE_TO}" downgraded_version="$(./yt-dlp_downgraded --version)" [[ "${version}" != "${downgraded_version}" ]] - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: build-bin-${{ github.job }} path: | yt-dlp yt-dlp.tar.gz compression-level: 0 linux: name: ${{ matrix.os }} (${{ matrix.arch }}) if: inputs.linux || inputs.linux_armv7l || inputs.musllinux needs: process runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: ${{ fromJSON(needs.process.outputs.linux_matrix) }} env: CHANNEL: ${{ inputs.channel }} ORIGIN: ${{ needs.process.outputs.origin }} VERSION: ${{ needs.process.outputs.version }} EXE_NAME: ${{ matrix.exe }} PYTHON_VERSION: ${{ matrix.python_version }} UPDATE_TO: ${{ (vars.UPDATE_TO_VERIFICATION && matrix.update_to) || '' }} SKIP_ONEDIR_BUILD: ${{ (!matrix.onedir && '1') || '' }} SKIP_ONEFILE_BUILD: ${{ (!matrix.onefile && '1') || '' }} steps: - uses: actions/checkout@v4 - name: Cache requirements if: matrix.cache_requirements id: cache-venv uses: actions/cache@v4 env: SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1 with: path: | venv key: cache-reqs-${{ matrix.os }}_${{ matrix.arch }}-${{ github.ref }}-${{ needs.process.outputs.timestamp }} restore-keys: | cache-reqs-${{ matrix.os }}_${{ matrix.arch }}-${{ github.ref }}- cache-reqs-${{ matrix.os }}_${{ matrix.arch }}- - name: Set up QEMU if: matrix.qemu_platform uses: docker/setup-qemu-action@v3 with: platforms: ${{ matrix.qemu_platform }} - name: Build executable env: SERVICE: ${{ matrix.os }}_${{ matrix.arch }} run: | mkdir -p ./venv mkdir -p ./dist pushd bundle/docker docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}" popd if [[ -z "${SKIP_ONEFILE_BUILD}" ]]; then sudo chown "${USER}:docker" "./dist/${EXE_NAME}" fi - name: Verify executable in container env: SERVICE: ${{ matrix.os }}_${{ matrix.arch }}_verify run: | cd bundle/docker docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}" - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: build-bin-${{ matrix.os }}_${{ matrix.arch }} path: | dist/${{ matrix.exe }}* compression-level: 0 macos: needs: process if: inputs.macos permissions: contents: read runs-on: macos-14 env: CHANNEL: ${{ inputs.channel }} ORIGIN: ${{ needs.process.outputs.origin }} VERSION: ${{ needs.process.outputs.version }} UPDATE_TO: yt-dlp/yt-dlp@2025.09.05 steps: - uses: actions/checkout@v4 # NB: Building universal2 does not work with python from actions/setup-python - name: Cache requirements id: cache-venv uses: actions/cache@v4 env: SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1 with: path: | ~/yt-dlp-build-venv key: cache-reqs-${{ github.job }}-${{ github.ref }}-${{ needs.process.outputs.timestamp }} restore-keys: | cache-reqs-${{ github.job }}-${{ github.ref }}- cache-reqs-${{ github.job }}- - name: Install Requirements run: | brew install coreutils # We need to use system Python in order to roll our own universal2 curl_cffi wheel brew uninstall --ignore-dependencies python3 python3 -m venv ~/yt-dlp-build-venv source ~/yt-dlp-build-venv/bin/activate python3 devscripts/install_deps.py -o --include build python3 devscripts/install_deps.py --print --include pyinstaller > requirements.txt # We need to ignore wheels otherwise we break universal2 builds python3 -m pip install -U --no-binary :all: -r requirements.txt # We need to fuse our own universal2 wheels for curl_cffi python3 -m pip install -U 'delocate==0.11.0' mkdir curl_cffi_whls curl_cffi_universal2 python3 devscripts/install_deps.py --print -o --include curl-cffi > requirements.txt for platform in "macosx_11_0_arm64" "macosx_11_0_x86_64"; do python3 -m pip download \ --only-binary=:all: \ --platform "${platform}" \ -d curl_cffi_whls \ -r requirements.txt done ( # Overwrite x86_64-only libs with fat/universal2 libs or else Pyinstaller will do the opposite # See https://github.com/yt-dlp/yt-dlp/pull/10069 cd curl_cffi_whls mkdir -p curl_cffi/.dylibs python_libdir=$(python3 -c 'import sys; from pathlib import Path; print(Path(sys.path[1]).parent)') for dylib in lib{ssl,crypto}.3.dylib; do cp "${python_libdir}/${dylib}" "curl_cffi/.dylibs/${dylib}" for wheel in curl_cffi*macos*x86_64.whl; do zip "${wheel}" "curl_cffi/.dylibs/${dylib}" done done ) python3 -m delocate.cmd.delocate_fuse curl_cffi_whls/curl_cffi*.whl -w curl_cffi_universal2 python3 -m delocate.cmd.delocate_fuse curl_cffi_whls/cffi*.whl -w curl_cffi_universal2 for wheel in curl_cffi_universal2/*cffi*.whl; do mv -n -- "${wheel}" "${wheel/x86_64/universal2}" done python3 -m pip install --force-reinstall -U curl_cffi_universal2/*cffi*.whl - name: Prepare run: | python3 devscripts/update-version.py -c "${CHANNEL}" -r "${ORIGIN}" "${VERSION}" python3 devscripts/make_lazy_extractors.py - name: Build run: | source ~/yt-dlp-build-venv/bin/activate python3 -m bundle.pyinstaller --target-architecture universal2 --onedir (cd ./dist/yt-dlp_macos && zip -r ../yt-dlp_macos.zip .) python3 -m bundle.pyinstaller --target-architecture universal2 - name: Verify --update-to if: vars.UPDATE_TO_VERIFICATION run: | chmod +x ./dist/yt-dlp_macos cp ./dist/yt-dlp_macos ./dist/yt-dlp_macos_downgraded version="$(./dist/yt-dlp_macos --version)" ./dist/yt-dlp_macos_downgraded -v --update-to "${UPDATE_TO}" downgraded_version="$(./dist/yt-dlp_macos_downgraded --version)" [[ "$version" != "$downgraded_version" ]] - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: build-bin-${{ github.job }} path: | dist/yt-dlp_macos dist/yt-dlp_macos.zip compression-level: 0 windows: name: windows (${{ matrix.arch }}) needs: process if: inputs.windows permissions: contents: read runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - arch: 'x64' runner: windows-2025 python_version: '3.10' platform_tag: win_amd64 pyi_version: '6.16.0' pyi_tag: '2025.09.13.221251' pyi_hash: b6496c7630c3afe66900cfa824e8234a8c2e2c81704bd7facd79586abc76c0e5 - arch: 'x86' runner: windows-2025 python_version: '3.10' platform_tag: win32 pyi_version: '6.16.0' pyi_tag: '2025.09.13.221251' pyi_hash: 2d881843580efdc54f3523507fc6d9c5b6051ee49c743a6d9b7003ac5758c226 - arch: 'arm64' runner: windows-11-arm python_version: '3.13' # arm64 only has Python >= 3.11 available platform_tag: win_arm64 pyi_version: '6.16.0' pyi_tag: '2025.09.13.221251' pyi_hash: 4250c9085e34a95c898f3ee2f764914fc36ec59f0d97c28e6a75fcf21f7b144f env: CHANNEL: ${{ inputs.channel }} ORIGIN: ${{ needs.process.outputs.origin }} VERSION: ${{ needs.process.outputs.version }} SUFFIX: ${{ (matrix.arch != 'x64' && format('_{0}', matrix.arch)) || '' }} UPDATE_TO: yt-dlp/yt-dlp@2025.09.05 BASE_CACHE_KEY: cache-reqs-${{ github.job }}_${{ matrix.arch }}-${{ matrix.python_version }} PYI_REPO: https://github.com/yt-dlp/Pyinstaller-Builds PYI_WHEEL: pyinstaller-${{ matrix.pyi_version }}-py3-none-${{ matrix.platform_tag }}.whl steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python_version }} architecture: ${{ matrix.arch }} - name: Cache requirements id: cache-venv if: matrix.arch == 'arm64' uses: actions/cache@v4 env: SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1 with: path: | /yt-dlp-build-venv key: ${{ env.BASE_CACHE_KEY }}-${{ github.ref }}-${{ needs.process.outputs.timestamp }} restore-keys: | ${{ env.BASE_CACHE_KEY }}-${{ github.ref }}- ${{ env.BASE_CACHE_KEY }}- - name: Install Requirements env: ARCH: ${{ matrix.arch }} PYI_URL: ${{ env.PYI_REPO }}/releases/download/${{ matrix.pyi_tag }}/${{ env.PYI_WHEEL }} PYI_HASH: ${{ matrix.pyi_hash }} shell: pwsh run: | python -m venv /yt-dlp-build-venv /yt-dlp-build-venv/Scripts/Activate.ps1 python -m pip install -U pip # Install custom PyInstaller build and verify hash mkdir /pyi-wheels python -m pip download -d /pyi-wheels --no-deps --require-hashes "pyinstaller@${Env:PYI_URL}#sha256=${Env:PYI_HASH}" python -m pip install --force-reinstall -U "/pyi-wheels/${Env:PYI_WHEEL}" python devscripts/install_deps.py -o --include build if ("${Env:ARCH}" -eq "x86") { python devscripts/install_deps.py } else { python devscripts/install_deps.py --include curl-cffi } - name: Prepare shell: pwsh run: | python devscripts/update-version.py -c "${Env:CHANNEL}" -r "${Env:ORIGIN}" "${Env:VERSION}" python devscripts/make_lazy_extractors.py - name: Build shell: pwsh run: | /yt-dlp-build-venv/Scripts/Activate.ps1 python -m bundle.pyinstaller python -m bundle.pyinstaller --onedir Compress-Archive -Path ./dist/yt-dlp${Env:SUFFIX}/* -DestinationPath ./dist/yt-dlp_win${Env:SUFFIX}.zip - name: Verify --update-to if: vars.UPDATE_TO_VERIFICATION shell: pwsh run: | $name = "yt-dlp${Env:SUFFIX}" Copy-Item "./dist/${name}.exe" "./dist/${name}_downgraded.exe" $version = & "./dist/${name}.exe" --version & "./dist/${name}_downgraded.exe" -v --update-to "${Env:UPDATE_TO}" $downgraded_version = & "./dist/${name}_downgraded.exe" --version if ($version -eq $downgraded_version) { exit 1 } - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: build-bin-${{ github.job }}-${{ matrix.arch }} path: | dist/yt-dlp${{ env.SUFFIX }}.exe dist/yt-dlp_win${{ env.SUFFIX }}.zip compression-level: 0 meta_files: if: always() && !cancelled() needs: - process - unix - linux - macos - windows runs-on: ubuntu-latest steps: - name: Download artifacts uses: actions/download-artifact@v4 with: path: artifact pattern: build-bin-* merge-multiple: true - name: Make SHA2-SUMS files run: | cd ./artifact/ # make sure SHA sums are also printed to stdout sha256sum -- * | tee ../SHA2-256SUMS sha512sum -- * | tee ../SHA2-512SUMS # also print as permanent annotations to the summary page while read -r shasum; do echo "::notice title=${shasum##* }::sha256: ${shasum% *}" done < ../SHA2-256SUMS - name: Make Update spec run: | cat >> _update_spec << EOF # This file is used for regulating self-update lock 2022.08.18.36 .+ Python 3\.6 lock 2023.11.16 (?!win_x86_exe).+ Python 3\.7 lock 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server) lock 2024.10.22 py2exe .+ lock 2024.10.22 zip Python 3\.8 lock 2024.10.22 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2) lock 2025.08.11 darwin_legacy_exe .+ lock 2025.08.27 linux_armv7l_exe .+ lockV2 yt-dlp/yt-dlp 2022.08.18.36 .+ Python 3\.6 lockV2 yt-dlp/yt-dlp 2023.11.16 (?!win_x86_exe).+ Python 3\.7 lockV2 yt-dlp/yt-dlp 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server) lockV2 yt-dlp/yt-dlp 2024.10.22 py2exe .+ lockV2 yt-dlp/yt-dlp 2024.10.22 zip Python 3\.8 lockV2 yt-dlp/yt-dlp 2024.10.22 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2) lockV2 yt-dlp/yt-dlp 2025.08.11 darwin_legacy_exe .+ lockV2 yt-dlp/yt-dlp 2025.08.27 linux_armv7l_exe .+ lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 (?!win_x86_exe).+ Python 3\.7 lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 win_x86_exe .+ Windows-(?:Vista|2008Server) lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 py2exe .+ lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 zip Python 3\.8 lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2) lockV2 yt-dlp/yt-dlp-nightly-builds 2025.08.12.233030 darwin_legacy_exe .+ lockV2 yt-dlp/yt-dlp-nightly-builds 2025.08.30.232839 linux_armv7l_exe .+ lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 (?!win_x86_exe).+ Python 3\.7 lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 win_x86_exe .+ Windows-(?:Vista|2008Server) lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.045052 py2exe .+ lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 zip Python 3\.8 lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2) lockV2 yt-dlp/yt-dlp-master-builds 2025.08.12.232447 darwin_legacy_exe .+ lockV2 yt-dlp/yt-dlp-master-builds 2025.09.05.212910 linux_armv7l_exe .+ EOF - name: Sign checksum files env: GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} if: env.GPG_SIGNING_KEY run: | gpg --batch --import <<< "${{ secrets.GPG_SIGNING_KEY }}" for signfile in ./SHA*SUMS; do gpg --batch --detach-sign "$signfile" done - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: build-${{ github.job }} path: | _update_spec SHA*SUMS* compression-level: 0 overwrite: true