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/<repo>
        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: {}

jobs:
  process:
    name: 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.14'
          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,
                  '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('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:
    name: unix
    needs: [process]
    if: inputs.unix
    permissions:
      contents: read
    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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          fetch-depth: 0  # Needed for changelog
          persist-credentials: false

      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1  # v6.3.0
        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-extra 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
        with:
          name: build-bin-${{ github.job }}
          path: |
            yt-dlp
            yt-dlp.tar.gz
          compression-level: 0

  linux:
    name: ${{ matrix.os }} (${{ matrix.arch }})
    needs: [process]
    if: inputs.linux || inputs.linux_armv7l || inputs.musllinux
    permissions:
      contents: read
    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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          persist-credentials: false

      - name: Set up QEMU
        if: matrix.qemu_platform
        uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8  # v4.2.0
        with:
          image: tonistiigi/binfmt:qemu-v10.0.4-56@sha256:30cc9a4d03765acac9be2ed0afc23af1ad018aed2c28ea4be8c2eb9afe03fbd1
          cache-image: false
          platforms: ${{ matrix.qemu_platform }}

      - name: Build executable
        env:
          SERVICE: ${{ matrix.os }}_${{ matrix.arch }}
        run: |
          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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
        with:
          name: build-bin-${{ matrix.os }}_${{ matrix.arch }}
          path: |
            dist/${{ matrix.exe }}*
          compression-level: 0

  macos:
    name: 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          persist-credentials: false

      # NB: Building universal2 does not work with python from actions/setup-python

      - 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 -m pip install -U --require-hashes -r "bundle/requirements/pip.txt"
          rm -rf build
          # Only directly install wheels for "macosx_10_15_universal2" and "any" platforms
          mkdir -p build/wheels
          python3 -m pip download \
            --only-binary=:all: \
            --platform=macosx_10_15_universal2 \
            --platform=any \
            -d build/wheels \
            --require-hashes \
            -r "bundle/requirements/macos.txt"
          python3 -m pip install --force-reinstall --no-deps -U build/wheels/*.whl
          rm -rf build/wheels/*
          # We need to fuse our own universal2 wheels for curl_cffi and cffi
          mkdir -p build/universal2
          for platform in "macosx_11_0_arm64" "macosx_11_0_x86_64"; do
            python3 -m pip download \
              --no-deps \
              --only-binary=:all: \
              --platform "${platform}" \
              -d build/wheels \
               --require-hashes \
              -r "bundle/requirements/macos-curl_cffi.txt"
          done
          python3 -m delocate.cmd.delocate_merge build/wheels/curl_cffi-*.whl -w build/universal2
          python3 -m delocate.cmd.delocate_merge build/wheels/cffi-*.whl -w build/universal2
          python3 -m pip install --force-reinstall --no-deps -U build/universal2/{curl_,}cffi-*.whl
          rm -rf build

      - 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
        with:
          name: build-bin-${{ github.job }}
          path: |
            dist/yt-dlp_macos
            dist/yt-dlp_macos.zip
          compression-level: 0

  macos_verify:
    name: Verify macos x86_64
    needs: [macos]
    if: inputs.macos && vars.UPDATE_TO_VERIFICATION
    permissions:
      contents: read
    runs-on: macos-15-intel
    env:
      UPDATE_TO: yt-dlp/yt-dlp@2025.09.05

    steps:
      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8.0.1
        with:
          path: dist
          pattern: build-bin-macos

      - name: Verify --update-to on x86_64 architecture
        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" ]]

  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'
        - arch: 'x86'
          runner: windows-2025
          python_version: '3.10'
        - arch: 'arm64'
          runner: windows-11-arm
          python_version: '3.13'  # arm64 only has Python >= 3.11 available
    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

    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          persist-credentials: false

      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1  # v6.3.0
        with:
          python-version: ${{ matrix.python_version }}
          architecture: ${{ matrix.arch }}

      - name: Install Requirements
        env:
          ARCH: ${{ matrix.arch }}
        shell: pwsh
        run: |
          $ErrorActionPreference = "Stop"
          $PSNativeCommandUseErrorActionPreference = $true
          python -m venv /yt-dlp-build-venv
          /yt-dlp-build-venv/Scripts/Activate.ps1
          python -m pip install -U --require-hashes -r "bundle/requirements/pip.txt"
          python -m pip install -U --require-hashes -r "bundle/requirements/win-${Env:ARCH}-pyinstaller.txt"
          if (${Env:ARCH} -eq "x86") {
            python -m pip install -U --require-hashes -r "bundle/requirements/default.txt"
          } else {
            python -m pip install -U --require-hashes -r "bundle/requirements/curl-cffi.txt"
          }

      - name: Prepare
        shell: pwsh
        run: |
          $ErrorActionPreference = "Stop"
          $PSNativeCommandUseErrorActionPreference = $true
          python devscripts/update-version.py -c "${Env:CHANNEL}" -r "${Env:ORIGIN}" "${Env:VERSION}"
          python devscripts/make_lazy_extractors.py

      - name: Build
        shell: pwsh
        run: |
          $ErrorActionPreference = "Stop"
          $PSNativeCommandUseErrorActionPreference = $true
          /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: |
          $ErrorActionPreference = "Stop"
          $PSNativeCommandUseErrorActionPreference = $true
          $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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
        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:
    name: Metadata files
    needs:
      - process
      - unix
      - linux
      - macos
      - macos_verify
      - windows
    if: always() && !failure() && !cancelled()
    runs-on: ubuntu-latest
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8.0.1
        with:
          path: artifact
          pattern: build-bin-*
          merge-multiple: true

      - name: Make SHA2-SUMS files
        shell: bash
        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 zip 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 .+
          lock 2025.10.14 zip Python 3\.9
          lockV2 yt-dlp/yt-dlp 2022.08.18.36 .+ Python 3\.6
          lockV2 yt-dlp/yt-dlp 2023.11.16 zip 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 2025.10.14 zip Python 3\.9
          lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 zip 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-nightly-builds 2025.10.14.232845 zip Python 3\.9
          lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 zip 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 .+
          lockV2 yt-dlp/yt-dlp-master-builds 2025.10.14.232330 zip Python 3\.9
          EOF

      - name: Sign checksum files
        env:
          GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
        if: env.GPG_SIGNING_KEY
        run: |
          gpg --batch --import <<< "${GPG_SIGNING_KEY}"
          for signfile in ./SHA*SUMS; do
            gpg --batch --detach-sign "$signfile"
          done

      - name: Upload artifacts
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
        with:
          name: build-${{ github.job }}
          path: |
            _update_spec
            SHA*SUMS*
          compression-level: 0
          overwrite: true
