diff --git a/.github/actions/artifact-download/action.yml b/.github/actions/artifact-download/action.yml new file mode 100644 index 0000000000..aa8220ca64 --- /dev/null +++ b/.github/actions/artifact-download/action.yml @@ -0,0 +1,223 @@ +# Hetzner-only. Callers pass org secrets via with: +# hetzner_access_key +# hetzner_secret_access_key +# hetzner_bucket +name: Download WebRTC build cache from Hetzner +description: Range-get a Hetzner build tarball, resume on retry, verify size, untar. + +inputs: + path: + description: Object name. Downloads artifacts//.tar + required: true + if_missing: + description: error fails when the object is absent; skip continues. + required: false + default: error + hetzner_access_key: + description: Hetzner object storage access key. + required: true + hetzner_secret_access_key: + description: Hetzner object storage secret access key. + required: true + hetzner_bucket: + description: Hetzner object storage bucket name. + required: true + +runs: + using: composite + steps: + - name: Ensure aws CLI + shell: bash + run: | + set -euo pipefail + if command -v aws >/dev/null; then + exit 0 + fi + if [[ "$(uname -s)" == Darwin ]]; then + brew install awscli + else + sudo apt-get update + sudo apt-get install -y awscli + fi + + - name: Download and extract build tarball + shell: bash + working-directory: ${{ github.workspace }} + env: + HETZNER_ACCESS_KEY_CI_ARTIFACTS: ${{ inputs.hetzner_access_key }} + HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS: ${{ inputs.hetzner_secret_access_key }} + HETZNER_BUCKET_CI_ARTIFACTS: ${{ inputs.hetzner_bucket }} + OBJECT_STEM: ${{ inputs.path }} + IF_MISSING: ${{ inputs.if_missing }} + run: | + set -euo pipefail + export AWS_ACCESS_KEY_ID="${HETZNER_ACCESS_KEY_CI_ARTIFACTS:?}" + export AWS_SECRET_ACCESS_KEY="${HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS:?}" + export AWS_DEFAULT_REGION=hel1 + export AWS_EC2_METADATA_DISABLED=true + export AWS_MAX_ATTEMPTS=10 + export AWS_RETRY_MODE=adaptive + endpoint="--endpoint-url https://hel1.your-objectstorage.com" + # Sequential get-object leaves a valid prefix on drop. + # s3 cp multipart may not, and it truncates dest on start. + # Peak disk is tar + extract (~2x); rm the tar after tar xf. + aws_timeouts=(--cli-connect-timeout 60 --cli-read-timeout 0) + BUCKET="${HETZNER_BUCKET_CI_ARTIFACTS:?}" + [[ -n "${OBJECT_STEM}" ]] + object="artifacts/${{ github.repository }}/${OBJECT_STEM}.tar" + tar_file="${RUNNER_TEMP:?}/${OBJECT_STEM}.tar" + partial="${tar_file}.partial" + chunk="${tar_file}.chunk" + trap 'rm -f "${tar_file}" "${partial}" "${chunk}"' EXIT + set +e + head_out="$(aws ${endpoint} "${aws_timeouts[@]}" s3api head-object \ + --bucket "${BUCKET}" --key "${object}" \ + --query ContentLength --output text 2>&1)" + head_rc=$? + set -e + if [[ "${head_rc}" -ne 0 ]]; then + if [[ "${IF_MISSING}" == "skip" ]] && \ + grep -qiE '404|Not Found|NoSuchKey|NotFound' <<<"${head_out}"; then + echo "missing ${object}; skipping extract" + exit 0 + fi + echo "::error::failed to access s3://${BUCKET}/${object}" + printf '%s\n' "${head_out}" + exit 1 + fi + expected_size="$(printf '%s' "${head_out}" | tr -d '[:space:]')" + if [[ ! "${expected_size}" =~ ^[0-9]+$ ]]; then + echo "::error::head-object missing ContentLength for ${object}" + printf '%s\n' "${head_out}" + exit 1 + fi + test -d src/.git + echo "downloading ${object} (${expected_size} bytes) -> ${tar_file}" + df -h "${GITHUB_WORKSPACE}" "${RUNNER_TEMP}" || true + file_size() { + if [[ "$(uname -s)" == Darwin ]]; then + stat -f %z "$1" + else + stat -c %s "$1" + fi + } + # 20.8 GiB Android tar; IncompleteRead after ~1–3 GiB + # (run 34870017073). Resume appended. 20 tries covers + # the 1.2 GiB worst chunk; remaining bytes are logged. + attempts=20 + download_ok=0 + for attempt in $(seq 1 "${attempts}"); do + if [[ -f "${partial}" ]]; then + have="$(file_size "${partial}")" + else + have=0 + fi + if [[ "${have}" -gt "${expected_size}" ]]; then + echo "partial ${have} > ${expected_size}; deleting" + rm -f "${partial}" + have=0 + fi + remaining=$((expected_size - have)) + echo "download attempt ${attempt}/${attempts} resume_from=${have} remaining=${remaining}" + if [[ "${have}" -lt "${expected_size}" ]]; then + rm -f "${chunk}" + start="${have}" + ( + last="${start}" + stall_logged=0 + t0="$(date +%s)" + while sleep 15; do + have_now=0 + if [[ -f "${partial}" ]]; then + have_now="$(file_size "${partial}")" + fi + if [[ -f "${chunk}" ]]; then + have_now=$((have_now + $(file_size "${chunk}"))) + fi + elapsed=$(( $(date +%s) - t0 )) + pct=0 + if [[ "${expected_size}" -gt 0 ]]; then + pct=$((have_now * 100 / expected_size)) + fi + echo "download progress: ${have_now} / ${expected_size} (${pct}%) resume_from=${start} elapsed=${elapsed}s" + if [[ "${have_now}" -eq "${last}" && "${stall_logged}" -eq 0 ]]; then + echo "download progress: size not growing yet (aws may buffer until GET completes)" + stall_logged=1 + fi + last="${have_now}" + done + ) & + prog_pid=$! + if [[ "${have}" -eq 0 ]]; then + set +e + aws ${endpoint} "${aws_timeouts[@]}" s3api get-object \ + --bucket "${BUCKET}" --key "${object}" \ + --range "bytes=0-" "${partial}" + dl_rc=$? + set -e + else + set +e + aws ${endpoint} "${aws_timeouts[@]}" s3api get-object \ + --bucket "${BUCKET}" --key "${object}" \ + --range "bytes=${have}-" "${chunk}" + dl_rc=$? + set -e + if [[ -f "${chunk}" ]]; then + chunk_size="$(file_size "${chunk}")" + if [[ "${chunk_size}" -gt 0 ]]; then + cat "${chunk}" >> "${partial}" + fi + rm -f "${chunk}" + fi + fi + kill "${prog_pid}" 2>/dev/null || true + wait "${prog_pid}" 2>/dev/null || true + if [[ "${dl_rc}" -ne 0 ]]; then + echo "download attempt ${attempt} failed (exit ${dl_rc})" + fi + fi + if [[ -f "${partial}" ]]; then + have="$(file_size "${partial}")" + else + have=0 + fi + remaining=$((expected_size - have)) + if [[ "${have}" -eq "${expected_size}" ]]; then + mv "${partial}" "${tar_file}" + download_ok=1 + break + fi + echo "size mismatch after attempt ${attempt}: local=${have} expected=${expected_size} remaining=${remaining}" + if [[ "${attempt}" -lt "${attempts}" ]]; then + sleep 10 + fi + done + if [[ "${download_ok}" -ne 1 ]]; then + actual_size="missing" + if [[ -f "${partial}" ]]; then + actual_size="$(file_size "${partial}")" + elif [[ -f "${tar_file}" ]]; then + actual_size="$(file_size "${tar_file}")" + fi + remaining="n/a" + if [[ "${actual_size}" =~ ^[0-9]+$ ]]; then + remaining=$((expected_size - actual_size)) + fi + echo "::error::failed to download s3://${BUCKET}/${object} after ${attempts} attempts (local=${actual_size} expected=${expected_size} remaining=${remaining})" + exit 1 + fi + echo "extracting ${tar_file} -> ${GITHUB_WORKSPACE}" + tar xf "${tar_file}" + rm -f "${tar_file}" + test -d src/.git + test -f src/DEPS + test ! -L src + printf '%s\n' "${expected_size}" > \ + "${GITHUB_WORKSPACE}/.hetzner-hit-bytes" + # Overlay members only. Do not touch .gclient-git-cache. + # -h: GNU/BSD no-dereference if a leftover symlink exists. + for p in src/resources .cipd; do + if [[ -d "${p}" ]]; then + find "${p}" -exec touch -h {} + + fi + done diff --git a/.github/actions/artifact-put/action.yml b/.github/actions/artifact-put/action.yml new file mode 100644 index 0000000000..7757c70ba4 --- /dev/null +++ b/.github/actions/artifact-put/action.yml @@ -0,0 +1,146 @@ +# Hetzner-only. Callers pass org secrets via with: +# hetzner_access_key +# hetzner_secret_access_key +# hetzner_bucket +name: Upload WebRTC build tarball to Hetzner +description: Pack git-cache plus reusable src/resources and .cipd; multipart-put. + +inputs: + path: + description: Object stem. Uploads artifacts//.tar + required: true + source: + description: DEPS_ROOT with .gclient-git-cache; optional src/resources and .cipd. + required: true + hetzner_access_key: + description: Hetzner object storage access key. + required: true + hetzner_secret_access_key: + description: Hetzner object storage secret access key. + required: true + hetzner_bucket: + description: Hetzner object storage bucket name. + required: true + +runs: + using: composite + steps: + - name: Ensure aws CLI + shell: bash + run: | + set -euo pipefail + if command -v aws >/dev/null; then + exit 0 + fi + if [[ "$(uname -s)" == Darwin ]]; then + brew install awscli + else + sudo apt-get update + sudo apt-get install -y awscli + fi + + - name: Upload build tarball to Hetzner + shell: bash + env: + HETZNER_ACCESS_KEY_CI_ARTIFACTS: ${{ inputs.hetzner_access_key }} + HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS: ${{ inputs.hetzner_secret_access_key }} + HETZNER_BUCKET_CI_ARTIFACTS: ${{ inputs.hetzner_bucket }} + OBJECT_STEM: ${{ inputs.path }} + SOURCE: ${{ inputs.source }} + run: | + set -euo pipefail + export AWS_ACCESS_KEY_ID="${HETZNER_ACCESS_KEY_CI_ARTIFACTS:?}" + export AWS_SECRET_ACCESS_KEY="${HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS:?}" + export AWS_DEFAULT_REGION=hel1 + export AWS_EC2_METADATA_DISABLED=true + export AWS_MAX_ATTEMPTS=10 + export AWS_RETRY_MODE=adaptive + endpoint="--endpoint-url https://hel1.your-objectstorage.com" + aws_timeouts=(--cli-connect-timeout 60 --cli-read-timeout 0) + [[ -n "${OBJECT_STEM}" ]] + [[ -n "${SOURCE}" ]] + test -d "${SOURCE}" + cache="${SOURCE}/.gclient-git-cache" + if [[ ! -d "${cache}" ]] || [[ -z "$(ls -A "${cache}")" ]]; then + echo "::error::build cache missing .gclient-git-cache" + exit 1 + fi + members=(.gclient-git-cache) + # Reusable only. Do not pack working trees or out/ (objects + # twice) or src/.git / Stream-tracked src files. setup-webrtc + # rewrites .gclient every job. + for p in src/resources .cipd; do + if [[ -d "${SOURCE}/${p}" ]] && \ + [[ -n "$(ls -A "${SOURCE}/${p}")" ]]; then + members+=("${p}") + else + echo "skip ${p} (missing or empty)" + fi + done + echo "pack members: ${members[*]}" + for p in "${members[@]}"; do + du -sk "${SOURCE}/${p}" + done + BUCKET="${HETZNER_BUCKET_CI_ARTIFACTS:?}" + object="artifacts/${{ github.repository }}/${OBJECT_STEM}.tar" + tar_file="${RUNNER_TEMP:?}/${OBJECT_STEM}.tar" + trap 'rm -f "${tar_file}"' EXIT + echo "packing ${SOURCE}/{${members[*]}} -> ${tar_file}" + tar cf "${tar_file}" -C "${SOURCE}" "${members[@]}" + if [[ "$(uname -s)" == Darwin ]]; then + size="$(stat -f %z "${tar_file}")" + else + size="$(stat -c %s "${tar_file}")" + fi + if [[ ! -f "${tar_file}" ]]; then + echo "::error::new tar missing ${tar_file}" + exit 1 + fi + hit_file="${GITHUB_WORKSPACE}/.hetzner-hit-bytes" + old="" + if [[ -f "${hit_file}" ]]; then + old="$(tr -d '[:space:]' < "${hit_file}")" + fi + threshold=1073741824 + skip_put=0 + if [[ "${old}" =~ ^[0-9]+$ ]]; then + if [[ "${size}" -ge "${old}" ]]; then + delta=$((size - old)) + else + delta=$((old - size)) + fi + if [[ "${delta}" -lt "${threshold}" ]]; then + echo "skip upload: old=${old} new=${size} delta=${delta} (< 1GiB)" + skip_put=1 + else + echo "upload: old=${old} new=${size} delta=${delta}" + fi + else + echo "upload: old=none new=${size} delta=n/a" + fi + if [[ "${skip_put}" -eq 0 ]]; then + df -h "${SOURCE}" "${RUNNER_TEMP}" || true + attempts=3 + upload_ok=0 + for attempt in $(seq 1 "${attempts}"); do + echo "upload attempt ${attempt}/${attempts}" + set +e + aws ${endpoint} "${aws_timeouts[@]}" \ + s3 cp "${tar_file}" "s3://${BUCKET}/${object}" + up_rc=$? + set -e + if [[ "${up_rc}" -eq 0 ]]; then + upload_ok=1 + break + fi + echo "upload attempt ${attempt} failed (exit ${up_rc})" + if [[ "${attempt}" -lt "${attempts}" ]]; then + sleep 10 + fi + done + if [[ "${upload_ok}" -ne 1 ]]; then + echo "::error::failed to upload s3://${BUCKET}/${object} after ${attempts} attempts (${size} bytes)" + exit 1 + fi + fi + rm -f "${tar_file}" diff --git a/.github/actions/prepare-common-v2/action.yml b/.github/actions/prepare-common-v2/action.yml new file mode 100644 index 0000000000..73185153f9 --- /dev/null +++ b/.github/actions/prepare-common-v2/action.yml @@ -0,0 +1,121 @@ +name: Plan platform deps +description: Map selected platforms to gclient target_os. + +inputs: + platform_ios: + required: true + description: Whether iOS is selected. + platform_macos: + required: true + description: Whether macOS is selected. + platform_android: + required: true + description: Whether Android is selected. + platform_windows: + required: false + default: "false" + description: Whether Windows is selected. + skip_maccatalyst: + required: false + default: "false" + description: Omit Mac Catalyst from the Apple deps label. + +outputs: + run_ios: + value: ${{ steps.flags.outputs.run_ios }} + run_macos: + value: ${{ steps.flags.outputs.run_macos }} + run_android: + value: ${{ steps.flags.outputs.run_android }} + run_windows: + value: ${{ steps.flags.outputs.run_windows }} + run_apple: + value: ${{ steps.flags.outputs.run_apple }} + apple_target_os: + value: ${{ steps.flags.outputs.apple_target_os }} + apple_target_os_label: + value: ${{ steps.flags.outputs.apple_target_os_label }} + android_target_os: + value: ${{ steps.flags.outputs.android_target_os }} + windows_target_os: + value: ${{ steps.flags.outputs.windows_target_os }} + linux_target_os: + value: ${{ steps.flags.outputs.linux_target_os }} + +runs: + using: composite + steps: + - id: flags + name: Resolve platform flags + shell: bash + run: | + set -euo pipefail + ios='${{ inputs.platform_ios }}' + macos='${{ inputs.platform_macos }}' + android='${{ inputs.platform_android }}' + windows='${{ inputs.platform_windows }}' + skip_maccatalyst='${{ inputs.skip_maccatalyst }}' + + # TARGET_OS is only tokens selected this run (ios, mac, android,unix, win). + apple_os_list=() + apple_label_list=() + linux_os_list=() + if [[ "${ios}" == "true" ]]; then + apple_os_list+=("ios") + apple_label_list+=("ios") + linux_os_list+=("ios") + fi + if [[ "${macos}" == "true" ]]; then + apple_os_list+=("mac") + apple_label_list+=("macos") + linux_os_list+=("mac") + fi + if [[ "${ios}" == "true" && "${skip_maccatalyst}" != "true" ]]; then + apple_label_list+=("maccatalyst") + fi + if [[ "${android}" == "true" ]]; then + linux_os_list+=("android" "unix") + fi + + join_csv() { + local IFS=, + printf '%s' "$*" + } + + run_apple=false + apple_target_os="" + apple_target_os_label="" + if [[ ${#apple_os_list[@]} -gt 0 ]]; then + run_apple=true + apple_target_os="$(join_csv "${apple_os_list[@]}")" + old_ifs="$IFS" + IFS=', ' + apple_target_os_label="${apple_label_list[*]}" + IFS="$old_ifs" + fi + + linux_target_os="" + if [[ ${#linux_os_list[@]} -gt 0 ]]; then + linux_target_os="$(join_csv "${linux_os_list[@]}")" + fi + + android_target_os="" + if [[ "${android}" == "true" ]]; then + android_target_os="android,unix" + fi + + windows_target_os="" + if [[ "${windows}" == "true" ]]; then + windows_target_os="win" + fi + + echo "run_ios=${ios}" >> "${GITHUB_OUTPUT}" + echo "run_macos=${macos}" >> "${GITHUB_OUTPUT}" + echo "run_android=${android}" >> "${GITHUB_OUTPUT}" + echo "run_windows=${windows}" >> "${GITHUB_OUTPUT}" + echo "run_apple=${run_apple}" >> "${GITHUB_OUTPUT}" + echo "apple_target_os=${apple_target_os}" >> "${GITHUB_OUTPUT}" + echo "apple_target_os_label=${apple_target_os_label}" >> "${GITHUB_OUTPUT}" + echo "android_target_os=${android_target_os}" >> "${GITHUB_OUTPUT}" + echo "windows_target_os=${windows_target_os}" >> "${GITHUB_OUTPUT}" + echo "linux_target_os=${linux_target_os}" >> "${GITHUB_OUTPUT}" diff --git a/.github/actions/restore-tree/action.yml b/.github/actions/restore-tree/action.yml new file mode 100644 index 0000000000..775d02490b --- /dev/null +++ b/.github/actions/restore-tree/action.yml @@ -0,0 +1,113 @@ +name: Restore WebRTC tree +description: HIT host build cache (or Windows GitHub deps), then make deps. + +inputs: + webrtc_ref: + required: true + description: Branch, tag, or SHA already checked out at path src. + target_os: + required: true + description: gclient TARGET_OS (selected tokens only). + cache_key: + required: false + default: "" + description: Hetzner stem (build-ios, build-macos, build-android). + skip_cache: + required: false + default: "false" + description: Skip Hetzner download (still make deps; caller uploads). + deps_artifact: + required: false + default: "" + description: GitHub artifact for Windows (deps-windows). + install_android_packages: + required: false + default: "false" + description: Install apt packages needed for Android builds. + hetzner_access_key: + required: false + default: "" + description: Hetzner object storage access key. + hetzner_secret_access_key: + required: false + default: "" + description: Hetzner object storage secret access key. + hetzner_bucket: + required: false + default: "" + description: Hetzner object storage bucket name. + +runs: + using: composite + steps: + - name: Setup WebRTC checkout + uses: ./src/.github/actions/setup-webrtc + with: + webrtc_ref: ${{ inputs.webrtc_ref }} + install_android_packages: ${{ inputs.install_android_packages }} + + # Checkout src first (caller). Extract git-cache to GIT_CACHE_PATH + # plus src/resources and .cipd. Do not strip git alternates; + # deps.sh rewrite_git_cache_alternates retargets packed cache + # paths. Missing object = cold make deps. + - name: Download host build cache + if: ${{ inputs.cache_key != '' && inputs.skip_cache != 'true' }} + uses: ./src/.github/actions/artifact-download + with: + path: ${{ inputs.cache_key }} + if_missing: skip + hetzner_access_key: ${{ inputs.hetzner_access_key }} + hetzner_secret_access_key: ${{ inputs.hetzner_secret_access_key }} + hetzner_bucket: ${{ inputs.hetzner_bucket }} + + - name: gclient sync and hooks + if: ${{ inputs.cache_key != '' }} + working-directory: src/stream_build + shell: bash + env: + DEPS_ROOT: ${{ github.workspace }} + WEBRTC_SRC: ${{ github.workspace }}/src + GIT_CACHE_PATH: ${{ github.workspace }}/.gclient-git-cache + TARGET_OS: ${{ inputs.target_os }} + JOBS: "8" + SHALLOW: "1" + RUN_HOOKS: "1" + WEBRTC_REPO: https://github.com/GetStream/webrtc.git + run: make deps + + # Windows is still a git-cache-only GitHub artifact. + - name: Download same-run deps artifact + if: ${{ inputs.deps_artifact != '' }} + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.deps_artifact }} + path: ${{ runner.temp }}/deps-git-cache + + - name: Install same-run gclient object cache + if: ${{ inputs.deps_artifact != '' }} + shell: bash + run: | + set -euo pipefail + dest="${GITHUB_WORKSPACE}/.gclient-git-cache" + src="${RUNNER_TEMP}/deps-git-cache" + if [[ -d "${src}/.gclient-git-cache" ]]; then + src="${src}/.gclient-git-cache" + fi + rm -rf "${dest}" + mv "${src}" "${dest}" + test -n "$(ls -A "${dest}")" + test -f "${GITHUB_WORKSPACE}/src/DEPS" + test ! -L "${GITHUB_WORKSPACE}/src" + + - name: gclient sync + if: ${{ inputs.deps_artifact != '' }} + working-directory: src/stream_build + shell: bash + env: + DEPS_ROOT: ${{ github.workspace }} + WEBRTC_SRC: ${{ github.workspace }}/src + GIT_CACHE_PATH: ${{ github.workspace }}/.gclient-git-cache + TARGET_OS: ${{ inputs.target_os }} + JOBS: "8" + WEBRTC_REPO: https://github.com/GetStream/webrtc.git + run: make deps diff --git a/.github/actions/setup-webrtc/action.yml b/.github/actions/setup-webrtc/action.yml new file mode 100644 index 0000000000..1f7b33738b --- /dev/null +++ b/.github/actions/setup-webrtc/action.yml @@ -0,0 +1,77 @@ +name: Setup WebRTC checkout +description: Install depot_tools against GITHUB_WORKSPACE/src (this checkout). No second clone. + +inputs: + webrtc_ref: + required: true + description: Branch, tag, or SHA already checked out at path src. + install_android_packages: + required: false + default: "false" + description: Install apt packages needed for Android builds. + +runs: + using: composite + steps: + # Caller checks out webrtc_ref with path: src. GITHUB_WORKSPACE is the + # webrtc-named gclient parent. + # gclient realpath()s gclient_gn_args_file (src/build/config/gclient_args.gni) + # and rejects a src that resolves outside DEPS_ROOT (depot_tools a6ba01c). + - name: Install Android host packages + if: ${{ inputs.install_android_packages == 'true' }} + shell: bash + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y python3-pip openjdk-8-jdk lsb-release software-properties-common + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.x" + + - name: Git authentication for HTTPS fetches + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + git config --global credential.helper store + git credential approve <> "${GITHUB_PATH}" + echo "DEPOT_TOOLS_UPDATE=0" >> "${GITHUB_ENV}" + echo "DEPS_ROOT=${GITHUB_WORKSPACE}" >> "${GITHUB_ENV}" + echo "WEBRTC_SRC=${GITHUB_WORKSPACE}/src" >> "${GITHUB_ENV}" + echo "WEBRTC_REPO=https://github.com/GetStream/webrtc.git" >> "${GITHUB_ENV}" + + - name: Ensure git-cache directory + shell: bash + run: | + set -euo pipefail + mkdir -p "${GITHUB_WORKSPACE}/.gclient-git-cache" + echo "GIT_CACHE_PATH=${GITHUB_WORKSPACE}/.gclient-git-cache" >> "${GITHUB_ENV}" + test -f "${GITHUB_WORKSPACE}/src/DEPS" + test -f "${GITHUB_WORKSPACE}/src/stream_build/Makefile" + test ! -L "${GITHUB_WORKSPACE}/src" + + - name: Chromium webrtc/src layout + working-directory: src/stream_build + shell: bash + env: + CONFIRM: "1" + run: make bootstrap diff --git a/.github/workflows/_make.yml b/.github/workflows/_make.yml new file mode 100644 index 0000000000..c9fe6f309d --- /dev/null +++ b/.github/workflows/_make.yml @@ -0,0 +1,785 @@ +# Called by Build v2 / Test v2 / Package v2 / Release v2 dispatch workflows. +# Not listed in the Actions dispatch UI (workflow_call only). +# Callers pass mode: build | test | package | release. +# +# I/O: iOS / macOS / Android jobs HIT Hetzner build-{ios,macos,android}.tar +# (skip_deps_cache skips download), make deps, make build|test, then put +# git-cache + src/resources + .cipd. No shared Linux Deps job. No GitHub +# deps-key. Windows Deps stays deps-windows on GitHub. Build mode is +# ninja only (no products). Package/Release Build jobs also make package +# and upload products-* (GitHub). Package combine consumes products-* +# (no third ninja) and uploads final-*. Release attaches final-*. Tests +# HIT the same per-platform key (out/ios_tests vs slice dirs). + +name: WebRTC make v2 + +on: + workflow_call: + inputs: + mode: + description: build, test, package, or release + required: true + type: string + webrtc_ref: + required: true + type: string + ios: + required: true + type: boolean + macos: + required: true + type: boolean + android: + required: true + type: boolean + windows: + required: true + type: boolean + config: + required: true + type: string + android_arch: + required: false + type: string + default: "" + version: + required: false + type: string + default: "" + prerelease: + required: false + type: boolean + default: false + release_notes: + required: false + type: string + default: "" + skip_deps_cache: + description: Skip Hetzner build cache download (cold make deps) + required: false + type: boolean + default: false + +env: + DEPS_ROOT: ${{ github.workspace }} + WEBRTC_SRC: ${{ github.workspace }}/src + GIT_CACHE_PATH: ${{ github.workspace }}/.gclient-git-cache + HETZNER_ACCESS_KEY_CI_ARTIFACTS: ${{ secrets.HETZNER_ACCESS_KEY_CI_ARTIFACTS }} + HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS: ${{ secrets.HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS }} + HETZNER_BUCKET_CI_ARTIFACTS: ${{ secrets.HETZNER_BUCKET_CI_ARTIFACTS }} + +jobs: + validate_inputs: + name: Validate inputs + runs-on: ubuntu-latest + steps: + - name: Ensure at least one platform is selected + env: + BUILD_IOS: ${{ inputs.ios }} + BUILD_MACOS: ${{ inputs.macos }} + BUILD_ANDROID: ${{ inputs.android }} + BUILD_WINDOWS: ${{ inputs.windows }} + MODE: ${{ inputs.mode }} + run: | + set -euo pipefail + case "${MODE}" in + build|test|package|release) ;; + *) + echo "mode must be build, test, package, or release (got '${MODE}')." + exit 1 + ;; + esac + if [[ "${BUILD_IOS}" != "true" && "${BUILD_MACOS}" != "true" && + "${BUILD_ANDROID}" != "true" && "${BUILD_WINDOWS}" != "true" ]]; then + echo "Select at least one platform." + exit 1 + fi + if [[ "${MODE}" == "test" && "${BUILD_ANDROID}" == "true" ]]; then + echo "make test android is not wired. Disable Android on Test." + exit 1 + fi + if [[ "${MODE}" == "release" && -z "${{ inputs.version }}" ]]; then + echo "Release requires a version." + exit 1 + fi + + - name: Validate Android build options + if: ${{ inputs.android }} + env: + ANDROID_ARCH: ${{ inputs.android_arch }} + run: | + set -euo pipefail + if [[ -n "${ANDROID_ARCH}" && "${ANDROID_ARCH}" =~ [[:space:]] ]]; then + echo "::error::android_arch accepts a single ABI, for example arm64-v8a." + exit 1 + fi + + plan: + name: Plan + needs: validate_inputs + runs-on: ubuntu-latest + outputs: + config: ${{ inputs.config }} + mode: ${{ inputs.mode }} + run_ios: ${{ steps.plan.outputs.run_ios }} + run_macos: ${{ steps.plan.outputs.run_macos }} + run_android: ${{ steps.plan.outputs.run_android }} + run_windows: ${{ steps.plan.outputs.run_windows }} + run_apple: ${{ steps.plan.outputs.run_apple }} + apple_target_os: ${{ steps.plan.outputs.apple_target_os }} + android_target_os: ${{ steps.plan.outputs.android_target_os }} + windows_target_os: ${{ steps.plan.outputs.windows_target_os }} + linux_target_os: ${{ steps.plan.outputs.linux_target_os }} + steps: + - uses: actions/checkout@v7 + with: + path: src + - id: plan + name: Plan target_os + uses: ./src/.github/actions/prepare-common-v2 + with: + platform_ios: ${{ inputs.ios }} + platform_macos: ${{ inputs.macos }} + platform_android: ${{ inputs.android }} + platform_windows: ${{ inputs.windows }} + + deps_windows: + name: Deps Windows + needs: plan + if: ${{ inputs.windows }} + runs-on: windows-latest + timeout-minutes: 360 + steps: + - uses: actions/checkout@v7 + with: + path: src + ref: ${{ inputs.webrtc_ref }} + - name: Check Windows WebRTC host + shell: bash + run: | + set -euo pipefail + missing=() + command -v make >/dev/null || missing+=("GNU make") + command -v python3 >/dev/null || missing+=("python3") + if [[ ${#missing[@]} -gt 0 ]]; then + echo "::error::Windows WebRTC host is not ready on windows-latest (missing: ${missing[*]}). This job is wired for make deps windows, but the runner image cannot run it yet." + exit 1 + fi + - name: Setup WebRTC checkout + uses: ./src/.github/actions/setup-webrtc + with: + webrtc_ref: ${{ inputs.webrtc_ref }} + - name: gclient sync (Windows) + working-directory: src/stream_build + shell: bash + env: + DEPS_ROOT: ${{ github.workspace }} + WEBRTC_SRC: ${{ github.workspace }}/src + GIT_CACHE_PATH: ${{ github.workspace }}/.gclient-git-cache + TARGET_OS: ${{ needs.plan.outputs.windows_target_os }} + JOBS: "8" + WEBRTC_REPO: https://github.com/GetStream/webrtc.git + run: make deps + - name: Upload deps-windows + uses: actions/upload-artifact@v7 + with: + name: deps-windows + path: .gclient-git-cache + include-hidden-files: true + if-no-files-found: error + retention-days: 1 + compression-level: 0 + + build_ios: + name: Build iOS v2 + needs: [plan] + if: ${{ (inputs.mode == 'build' || inputs.mode == 'package' || inputs.mode == 'release') && inputs.ios }} + runs-on: macos-26 + timeout-minutes: 360 + steps: + - uses: actions/checkout@v7 + with: + path: src + ref: ${{ inputs.webrtc_ref }} + - uses: ./src/.github/actions/restore-tree + with: + webrtc_ref: ${{ inputs.webrtc_ref }} + target_os: ios + cache_key: build-ios + skip_cache: ${{ inputs.skip_deps_cache }} + hetzner_access_key: ${{ secrets.HETZNER_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_secret_access_key: ${{ secrets.HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_bucket: ${{ secrets.HETZNER_BUCKET_CI_ARTIFACTS }} + - name: Build iOS + working-directory: src/stream_build + env: + DEPS_ROOT: ${{ github.workspace }} + PRODUCTS: ${{ github.workspace }}/products + CONFIG: ${{ inputs.config }} + SKIP_DEPS: "1" + run: make build ios + - name: Upload build-ios to Hetzner + uses: ./src/.github/actions/artifact-put + with: + path: build-ios + source: ${{ github.workspace }} + hetzner_access_key: ${{ secrets.HETZNER_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_secret_access_key: ${{ secrets.HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_bucket: ${{ secrets.HETZNER_BUCKET_CI_ARTIFACTS }} + - name: Package iOS + if: ${{ inputs.mode == 'package' || inputs.mode == 'release' }} + working-directory: src/stream_build + env: + DEPS_ROOT: ${{ github.workspace }} + PRODUCTS: ${{ github.workspace }}/products + CONFIG: ${{ inputs.config }} + SKIP_DEPS: "1" + run: make package ios + - name: Upload products-ios + if: ${{ inputs.mode == 'package' || inputs.mode == 'release' }} + uses: actions/upload-artifact@v7 + with: + name: products-ios + path: products/ios + if-no-files-found: error + retention-days: 7 + + build_macos: + name: Build macOS v2 + needs: [plan] + if: ${{ (inputs.mode == 'build' || inputs.mode == 'package' || inputs.mode == 'release') && inputs.macos }} + runs-on: macos-26 + timeout-minutes: 360 + steps: + - uses: actions/checkout@v7 + with: + path: src + ref: ${{ inputs.webrtc_ref }} + - uses: ./src/.github/actions/restore-tree + with: + webrtc_ref: ${{ inputs.webrtc_ref }} + target_os: mac + cache_key: build-macos + skip_cache: ${{ inputs.skip_deps_cache }} + hetzner_access_key: ${{ secrets.HETZNER_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_secret_access_key: ${{ secrets.HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_bucket: ${{ secrets.HETZNER_BUCKET_CI_ARTIFACTS }} + - name: Build macOS + working-directory: src/stream_build + env: + DEPS_ROOT: ${{ github.workspace }} + PRODUCTS: ${{ github.workspace }}/products + CONFIG: ${{ inputs.config }} + SKIP_DEPS: "1" + run: make build macos + - name: Upload build-macos to Hetzner + uses: ./src/.github/actions/artifact-put + with: + path: build-macos + source: ${{ github.workspace }} + hetzner_access_key: ${{ secrets.HETZNER_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_secret_access_key: ${{ secrets.HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_bucket: ${{ secrets.HETZNER_BUCKET_CI_ARTIFACTS }} + - name: Package macOS + if: ${{ inputs.mode == 'package' || inputs.mode == 'release' }} + working-directory: src/stream_build + env: + DEPS_ROOT: ${{ github.workspace }} + PRODUCTS: ${{ github.workspace }}/products + CONFIG: ${{ inputs.config }} + SKIP_DEPS: "1" + run: make package macos + - name: Upload products-macos + if: ${{ inputs.mode == 'package' || inputs.mode == 'release' }} + uses: actions/upload-artifact@v7 + with: + name: products-macos + path: products/macos + if-no-files-found: error + retention-days: 7 + + build_android: + name: Build Android v2 + needs: [plan] + if: ${{ (inputs.mode == 'build' || inputs.mode == 'package' || inputs.mode == 'release') && inputs.android }} + runs-on: ubuntu-latest + timeout-minutes: 360 + steps: + - uses: actions/checkout@v7 + with: + path: src + ref: ${{ inputs.webrtc_ref }} + - uses: ./src/.github/actions/restore-tree + with: + webrtc_ref: ${{ inputs.webrtc_ref }} + target_os: ${{ needs.plan.outputs.android_target_os }} + cache_key: build-android + skip_cache: ${{ inputs.skip_deps_cache }} + install_android_packages: "true" + hetzner_access_key: ${{ secrets.HETZNER_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_secret_access_key: ${{ secrets.HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_bucket: ${{ secrets.HETZNER_BUCKET_CI_ARTIFACTS }} + - name: Build Android + working-directory: src/stream_build + env: + DEPS_ROOT: ${{ github.workspace }} + PRODUCTS: ${{ github.workspace }}/products + CONFIG: ${{ inputs.config }} + SKIP_DEPS: "1" + ANDROID_ARCH: ${{ inputs.android_arch }} + run: | + set -euo pipefail + extra=() + if [[ -n "${ANDROID_ARCH}" ]]; then + extra+=(ARCHS="${ANDROID_ARCH}") + fi + make build android "${extra[@]}" + - name: Upload build-android to Hetzner + uses: ./src/.github/actions/artifact-put + with: + path: build-android + source: ${{ github.workspace }} + hetzner_access_key: ${{ secrets.HETZNER_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_secret_access_key: ${{ secrets.HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_bucket: ${{ secrets.HETZNER_BUCKET_CI_ARTIFACTS }} + - name: Package Android + if: ${{ inputs.mode == 'package' || inputs.mode == 'release' }} + working-directory: src/stream_build + env: + DEPS_ROOT: ${{ github.workspace }} + PRODUCTS: ${{ github.workspace }}/products + CONFIG: ${{ inputs.config }} + SKIP_DEPS: "1" + ANDROID_ARCH: ${{ inputs.android_arch }} + run: | + set -euo pipefail + extra=() + if [[ -n "${ANDROID_ARCH}" ]]; then + extra+=(ARCHS="${ANDROID_ARCH}") + fi + make package android "${extra[@]}" + - name: Upload products-android + if: ${{ inputs.mode == 'package' || inputs.mode == 'release' }} + uses: actions/upload-artifact@v7 + with: + name: products-android + path: products + if-no-files-found: error + retention-days: 7 + + test_ios: + name: Test iOS + needs: [plan] + if: ${{ (inputs.mode == 'test' || inputs.mode == 'release') && inputs.ios }} + runs-on: macos-26 + timeout-minutes: 180 + steps: + - uses: actions/checkout@v7 + with: + path: src + ref: ${{ inputs.webrtc_ref }} + - uses: ./src/.github/actions/restore-tree + with: + webrtc_ref: ${{ inputs.webrtc_ref }} + target_os: ios + cache_key: build-ios + skip_cache: ${{ inputs.skip_deps_cache }} + hetzner_access_key: ${{ secrets.HETZNER_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_secret_access_key: ${{ secrets.HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_bucket: ${{ secrets.HETZNER_BUCKET_CI_ARTIFACTS }} + - name: Test iOS + working-directory: src/stream_build + env: + DEPS_ROOT: ${{ github.workspace }} + SKIP_DEPS: "1" + run: make test ios + - name: Upload build-ios to Hetzner + if: ${{ inputs.mode == 'test' }} + uses: ./src/.github/actions/artifact-put + with: + path: build-ios + source: ${{ github.workspace }} + hetzner_access_key: ${{ secrets.HETZNER_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_secret_access_key: ${{ secrets.HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_bucket: ${{ secrets.HETZNER_BUCKET_CI_ARTIFACTS }} + + test_macos: + name: Test macOS + needs: [plan] + if: ${{ (inputs.mode == 'test' || inputs.mode == 'release') && inputs.macos }} + runs-on: macos-26 + timeout-minutes: 180 + steps: + - uses: actions/checkout@v7 + with: + path: src + ref: ${{ inputs.webrtc_ref }} + - uses: ./src/.github/actions/restore-tree + with: + webrtc_ref: ${{ inputs.webrtc_ref }} + target_os: mac + cache_key: build-macos + skip_cache: ${{ inputs.skip_deps_cache }} + hetzner_access_key: ${{ secrets.HETZNER_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_secret_access_key: ${{ secrets.HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_bucket: ${{ secrets.HETZNER_BUCKET_CI_ARTIFACTS }} + - name: Test macOS + working-directory: src/stream_build + env: + DEPS_ROOT: ${{ github.workspace }} + SKIP_DEPS: "1" + run: make test macos + - name: Upload build-macos to Hetzner + if: ${{ inputs.mode == 'test' }} + uses: ./src/.github/actions/artifact-put + with: + path: build-macos + source: ${{ github.workspace }} + hetzner_access_key: ${{ secrets.HETZNER_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_secret_access_key: ${{ secrets.HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS }} + hetzner_bucket: ${{ secrets.HETZNER_BUCKET_CI_ARTIFACTS }} + + test_windows: + name: Test Windows + needs: [plan, deps_windows] + if: ${{ (inputs.mode == 'test' || inputs.mode == 'release') && inputs.windows }} + runs-on: windows-latest + timeout-minutes: 360 + steps: + - uses: actions/checkout@v7 + with: + path: src + ref: ${{ inputs.webrtc_ref }} + - name: Check Windows WebRTC host + shell: bash + run: | + set -euo pipefail + missing=() + command -v make >/dev/null || missing+=("GNU make") + command -v python3 >/dev/null || missing+=("python3") + if [[ ${#missing[@]} -gt 0 ]]; then + echo "::error::Windows WebRTC host is not ready on windows-latest (missing: ${missing[*]}). This job is wired for make test windows, but the runner image cannot run it yet." + exit 1 + fi + - uses: ./src/.github/actions/restore-tree + with: + webrtc_ref: ${{ inputs.webrtc_ref }} + target_os: ${{ needs.plan.outputs.windows_target_os }} + deps_artifact: deps-windows + - name: Test Windows + working-directory: src/stream_build + shell: bash + env: + DEPS_ROOT: ${{ github.workspace }} + SKIP_DEPS: "1" + run: make test windows + + tests_passed: + name: Tests passed + needs: [plan, test_ios, test_macos, test_windows] + if: ${{ always() && !cancelled() && inputs.mode == 'release' }} + runs-on: ubuntu-latest + steps: + - name: Require tests before publish + env: + WANT_IOS: ${{ inputs.ios }} + WANT_MACOS: ${{ inputs.macos }} + WANT_WINDOWS: ${{ inputs.windows }} + IOS_RESULT: ${{ needs.test_ios.result }} + MACOS_RESULT: ${{ needs.test_macos.result }} + WINDOWS_RESULT: ${{ needs.test_windows.result }} + run: | + set -euo pipefail + failed=0 + if [[ "${WANT_IOS}" == "true" && "${IOS_RESULT}" != "success" ]]; then + echo "::error::Release is blocked: iOS tests ${IOS_RESULT}." + failed=1 + fi + if [[ "${WANT_MACOS}" == "true" && "${MACOS_RESULT}" != "success" ]]; then + echo "::error::Release is blocked: macOS tests ${MACOS_RESULT}." + failed=1 + fi + if [[ "${WANT_WINDOWS}" == "true" && "${WINDOWS_RESULT}" != "success" ]]; then + echo "::error::Release is blocked: Windows tests ${WINDOWS_RESULT}." + failed=1 + fi + if [[ "${failed}" -ne 0 ]]; then + echo "If tests fail, do not publish." + exit 1 + fi + echo "Selected-platform tests passed (Android tests are unwired and skipped)." + + build_windows: + name: Build Windows v2 + needs: [plan, deps_windows] + if: ${{ (inputs.mode == 'build' || inputs.mode == 'package' || inputs.mode == 'release') && inputs.windows }} + runs-on: windows-latest + timeout-minutes: 360 + steps: + - uses: actions/checkout@v7 + with: + path: src + ref: ${{ inputs.webrtc_ref }} + - name: Check Windows WebRTC host + shell: bash + run: | + set -euo pipefail + missing=() + command -v make >/dev/null || missing+=("GNU make") + command -v python3 >/dev/null || missing+=("python3") + if [[ ${#missing[@]} -gt 0 ]]; then + echo "::error::Windows WebRTC host is not ready on windows-latest (missing: ${missing[*]}). This job is wired for make build windows, but the runner image cannot run it yet." + exit 1 + fi + - uses: ./src/.github/actions/restore-tree + with: + webrtc_ref: ${{ inputs.webrtc_ref }} + target_os: ${{ needs.plan.outputs.windows_target_os }} + deps_artifact: deps-windows + - name: Build Windows + working-directory: src/stream_build + shell: bash + env: + DEPS_ROOT: ${{ github.workspace }} + PRODUCTS: ${{ github.workspace }}/products + CONFIG: ${{ inputs.config }} + SKIP_DEPS: "1" + run: make build windows + - name: Package Windows + if: ${{ inputs.mode == 'package' || inputs.mode == 'release' }} + working-directory: src/stream_build + shell: bash + env: + DEPS_ROOT: ${{ github.workspace }} + PRODUCTS: ${{ github.workspace }}/products + CONFIG: ${{ inputs.config }} + SKIP_DEPS: "1" + run: make package windows + - name: Upload products-windows + if: ${{ inputs.mode == 'package' || inputs.mode == 'release' }} + uses: actions/upload-artifact@v7 + with: + name: products-windows + path: products/windows + if-no-files-found: error + retention-days: 7 + + package: + name: Package + needs: [plan, build_ios, build_macos, build_android, build_windows] + if: ${{ always() && !cancelled() && (inputs.mode == 'package' || inputs.mode == 'release') && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && (needs.build_ios.result == 'success' || needs.build_macos.result == 'success' || needs.build_android.result == 'success' || needs.build_windows.result == 'success') }} + runs-on: ${{ (inputs.ios || inputs.macos) && 'macos-26' || 'ubuntu-latest' }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@v7 + with: + path: src + + - name: Chromium webrtc/src layout + working-directory: src/stream_build + env: + CONFIRM: "1" + run: make bootstrap + + - name: Download product artifacts + uses: actions/download-artifact@v8 + with: + pattern: products-* + path: combine-in + + - name: Combine Apple xcframeworks + if: ${{ inputs.ios || inputs.macos }} + working-directory: src/stream_build + env: + PRODUCTS: ${{ github.workspace }}/combine-in + SKIP_LICENSES: "1" + run: make combine + + - name: Attach LICENSE.md and zip Apple artifact + if: ${{ inputs.ios || inputs.macos }} + run: | + set -euo pipefail + dest="${GITHUB_WORKSPACE}/combine-in/WebRTC.xcframework" + test -d "${dest}" + if [[ ! -f "${dest}/LICENSE.md" ]]; then + license="$(find "${GITHUB_WORKSPACE}/combine-in" -path '*/WebRTC.xcframework/LICENSE.md' ! -path "${dest}/LICENSE.md" | head -n 1 || true)" + if [[ -n "${license}" ]]; then + cp "${license}" "${dest}/LICENSE.md" + fi + fi + ditto -c -k --sequesterRsrc --keepParent \ + "${dest}" \ + "${GITHUB_WORKSPACE}/WebRTC.xcframework.zip" + + - name: Pass through Android AAR + if: ${{ inputs.android }} + run: | + set -euo pipefail + src="$(find "${GITHUB_WORKSPACE}/combine-in" -name libwebrtc.aar -type f | head -n 1)" + test -n "${src}" + cp "${src}" "${GITHUB_WORKSPACE}/libwebrtc.aar" + mkdir -p "${GITHUB_WORKSPACE}/final-android" + cp "${src}" "${GITHUB_WORKSPACE}/final-android/libwebrtc.aar" + license="$(find "${GITHUB_WORKSPACE}/combine-in" -name LICENSE.md -type f | head -n 1 || true)" + if [[ -n "${license}" ]]; then + cp "${license}" "${GITHUB_WORKSPACE}/final-android/LICENSE.md" + fi + + - name: Pass through Windows products + if: ${{ inputs.windows }} + run: | + set -euo pipefail + mkdir -p "${GITHUB_WORKSPACE}/windows-libs" + cp -R "${GITHUB_WORKSPACE}/combine-in/products-windows/." "${GITHUB_WORKSPACE}/windows-libs/" + + - name: Rename copies for wrapper SDKs + if: ${{ inputs.mode == 'release' }} + working-directory: src/stream_build + env: + PRODUCTS: ${{ github.workspace }}/combine-in + run: | + set -euo pipefail + if [[ "${{ inputs.ios }}" == "true" || "${{ inputs.macos }}" == "true" ]]; then + make rename apple XCFRAMEWORK="${PRODUCTS}/WebRTC.xcframework" + ditto -c -k --sequesterRsrc --keepParent \ + "${PRODUCTS}/renamed/StreamWebRTC.xcframework" \ + "${GITHUB_WORKSPACE}/StreamWebRTC.xcframework.zip" + fi + if [[ "${{ inputs.android }}" == "true" ]]; then + make rename android AAR="${GITHUB_WORKSPACE}/libwebrtc.aar" + cp "${PRODUCTS}/renamed/libwebrtc.aar" \ + "${GITHUB_WORKSPACE}/libwebrtc-renamed.aar" + fi + + - name: Upload WebRTC.xcframework.zip + if: ${{ inputs.ios || inputs.macos }} + uses: actions/upload-artifact@v7 + with: + name: final-apple + path: WebRTC.xcframework.zip + if-no-files-found: error + retention-days: 7 + + - name: Upload libwebrtc.aar + if: ${{ inputs.android }} + uses: actions/upload-artifact@v7 + with: + name: final-android + path: final-android + if-no-files-found: error + retention-days: 7 + + - name: Upload Windows libs + if: ${{ inputs.windows }} + uses: actions/upload-artifact@v7 + with: + name: final-windows + path: windows-libs + if-no-files-found: error + retention-days: 7 + + - name: Upload StreamWebRTC.xcframework.zip + if: ${{ inputs.mode == 'release' && (inputs.ios || inputs.macos) }} + uses: actions/upload-artifact@v7 + with: + name: final-apple-renamed + path: StreamWebRTC.xcframework.zip + if-no-files-found: error + retention-days: 7 + + - name: Upload renamed Android AAR + if: ${{ inputs.mode == 'release' && inputs.android }} + uses: actions/upload-artifact@v7 + with: + name: final-android-renamed + path: libwebrtc-renamed.aar + if-no-files-found: error + retention-days: 7 + + github_release: + name: Release + needs: [plan, tests_passed, package] + if: ${{ always() && !cancelled() && inputs.mode == 'release' && needs.tests_passed.result == 'success' && needs.package.result == 'success' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + path: src + ref: ${{ inputs.webrtc_ref }} + + - name: Download finalised artifacts + uses: actions/download-artifact@v8 + with: + pattern: final-* + merge-multiple: true + path: release-assets + + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ inputs.version }} + RELEASE_REF: ${{ inputs.webrtc_ref }} + IS_PRERELEASE: ${{ inputs.prerelease }} + RELEASE_NOTES: ${{ inputs.release_notes }} + run: | + set -euo pipefail + shopt -s nullglob + assets=(release-assets/*) + if [[ ${#assets[@]} -eq 0 ]]; then + echo "::error::No finalised artifacts to attach." + exit 1 + fi + notes_file="$(mktemp)" + if [[ -n "${RELEASE_NOTES}" ]]; then + printf '%s\n' "${RELEASE_NOTES}" > "${notes_file}" + else + printf 'Automated WebRTC SDK release %s.\n' "${RELEASE_VERSION}" > "${notes_file}" + fi + release_args=( + "${RELEASE_VERSION}" + "${assets[@]}" + --repo "${{ github.repository }}" + --target "${RELEASE_REF}" + --title "${RELEASE_VERSION}" + --notes-file "${notes_file}" + ) + if [[ "${IS_PRERELEASE}" == "true" ]]; then + release_args+=(--prerelease) + else + release_args+=(--latest) + fi + gh release create "${release_args[@]}" + + trigger_downstream_releases: + name: Trigger downstream WebRTC releases v2 + needs: [plan, github_release] + if: ${{ always() && !cancelled() && inputs.mode == 'release' && needs.github_release.result == 'success' }} + runs-on: ubuntu-latest + steps: + - name: Trigger stream-video-swift-webrtc release + if: ${{ inputs.ios || inputs.macos }} + env: + GH_TOKEN: ${{ secrets.CROSS_REPO_TRIGGER_RELEASE_TOKEN }} + RELEASE_VERSION: ${{ inputs.version }} + IS_PRERELEASE: ${{ inputs.prerelease }} + run: | + set -euo pipefail + webrtc_release_url="https://github.com/${{ github.repository }}/releases/tag/${RELEASE_VERSION}" + gh workflow run publish-from-webrtc.yml \ + --repo GetStream/stream-video-swift-webrtc \ + --field "webrtc_release_url=${webrtc_release_url}" \ + --field "pre_release=${IS_PRERELEASE}" + + - name: Trigger stream-video-android-webrtc release + if: ${{ inputs.android }} + env: + GH_TOKEN: ${{ secrets.CROSS_REPO_TRIGGER_RELEASE_TOKEN }} + RELEASE_VERSION: ${{ inputs.version }} + IS_PRERELEASE: ${{ inputs.prerelease }} + run: | + set -euo pipefail + webrtc_release_url="https://github.com/${{ github.repository }}/releases/tag/${RELEASE_VERSION}" + gh workflow run publish-from-webrtc.yml \ + --repo GetStream/stream-video-android-webrtc \ + --field "webrtc_release_url=${webrtc_release_url}" \ + --field "pre_release=${IS_PRERELEASE}" diff --git a/.github/workflows/build-v2.yml b/.github/workflows/build-v2.yml new file mode 100644 index 0000000000..d000a38a1f --- /dev/null +++ b/.github/workflows/build-v2.yml @@ -0,0 +1,68 @@ +# Manual WebRTC build (v2 Makefile DAG). Implementation: +# .github/workflows/_make.yml +# Build iOS / macOS / Android in parallel: HIT build-{ios,macos,android}, +# make deps, make build, put that host's tree + out/. No Linux Deps job. +# No package combine, no products. + +name: Build v2 + +run-name: >- + Build v2 config:${{ inputs.config }}${{ inputs.ios == 'true' && ' iOS' || '' }}${{ inputs.macos == 'true' && (inputs.ios == 'true' && ', macOS' || ' macOS') || '' }}${{ inputs.android == 'true' && format('{0} Android{1}', (inputs.ios == 'true' || inputs.macos == 'true') && ',' || '', inputs.android_arch != '' && format(' ({0})', inputs.android_arch) || '') || '' }}${{ inputs.windows == 'true' && ((inputs.ios == 'true' || inputs.macos == 'true' || inputs.android == 'true') && ', Windows' || ' Windows') || '' }} + +permissions: + contents: read + +on: + workflow_dispatch: + inputs: + webrtc_ref: + description: Branch, tag, or SHA for this repo (GetStream/webrtc) + required: true + default: main + ios: + description: Build iOS + type: boolean + default: true + macos: + description: Build macOS + type: boolean + default: true + android: + description: Build Android + type: boolean + default: true + windows: + description: Build Windows (fails clearly if the runner is not a WebRTC host) + type: boolean + default: false + config: + description: GN configuration + type: choice + options: + - release + - debug + default: release + android_arch: + description: Optional Android ABI, for example arm64-v8a. Empty = default ABI set. + required: false + default: "" + skip_deps_cache: + description: Skip Hetzner deps cache (fresh gclient + ninja) + type: boolean + default: false + +jobs: + build: + name: Build v2 + uses: ./.github/workflows/_make.yml + secrets: inherit + with: + mode: build + webrtc_ref: ${{ inputs.webrtc_ref }} + ios: ${{ inputs.ios }} + macos: ${{ inputs.macos }} + android: ${{ inputs.android }} + windows: ${{ inputs.windows }} + config: ${{ inputs.config }} + android_arch: ${{ inputs.android_arch }} + skip_deps_cache: ${{ inputs.skip_deps_cache }} diff --git a/.github/workflows/package-v2.yml b/.github/workflows/package-v2.yml new file mode 100644 index 0000000000..029b0a3a6a --- /dev/null +++ b/.github/workflows/package-v2.yml @@ -0,0 +1,62 @@ +# Package WebRTC artifacts (v2 Makefile DAG). Does not create a GitHub +# release. _make.yml: Build (HIT + make deps + make build + make package, +# upload products-*) → Package combine (final-*). No Linux Deps job. No +# Test. No GH release. + +name: Package v2 + +run-name: >- + Package v2 config:${{ inputs.config }}${{ inputs.ios == 'true' && ' iOS' || '' }}${{ inputs.macos == 'true' && (inputs.ios == 'true' && ', macOS' || ' macOS') || '' }}${{ inputs.android == 'true' && format('{0} Android{1}', (inputs.ios == 'true' || inputs.macos == 'true') && ',' || '', inputs.android_arch != '' && format(' ({0})', inputs.android_arch) || '') || '' }}${{ inputs.windows == 'true' && ((inputs.ios == 'true' || inputs.macos == 'true' || inputs.android == 'true') && ', Windows' || ' Windows') || '' }} + +permissions: + contents: read + +on: + workflow_dispatch: + inputs: + webrtc_ref: + description: Branch, tag, or SHA for this repo (GetStream/webrtc) + required: true + default: main + ios: + description: Package iOS xcframework + type: boolean + default: true + macos: + description: Package macOS xcframework + type: boolean + default: true + android: + description: Package Android AAR + type: boolean + default: true + windows: + description: Package Windows libs (fails clearly if the runner is not a WebRTC host) + type: boolean + default: false + config: + description: GN configuration + type: choice + options: + - release + - debug + default: release + android_arch: + description: Optional Android ABI, for example arm64-v8a. Empty = default ABI set. + required: false + default: "" + +jobs: + package: + name: Package v2 + uses: ./.github/workflows/_make.yml + secrets: inherit + with: + mode: package + webrtc_ref: ${{ inputs.webrtc_ref }} + ios: ${{ inputs.ios }} + macos: ${{ inputs.macos }} + android: ${{ inputs.android }} + windows: ${{ inputs.windows }} + config: ${{ inputs.config }} + android_arch: ${{ inputs.android_arch }} diff --git a/.github/workflows/release-v2.yml b/.github/workflows/release-v2.yml new file mode 100644 index 0000000000..6976414d04 --- /dev/null +++ b/.github/workflows/release-v2.yml @@ -0,0 +1,76 @@ +# GitHub release of packaged WebRTC artifacts (v2 Makefile DAG), then +# downstream wrapper publishes. Not dispatchable until this file exists +# on the default branch. Tests run parallel with Build (each HIT its +# platform cache). Publish waits on Test + Package. + +name: Release v2 + +run-name: >- + Release v2 config:${{ inputs.config }}${{ inputs.prerelease == 'true' && ' Pre-Release' || '' }} ${{ inputs.version }}${{ inputs.ios == 'true' && ' iOS' || '' }}${{ inputs.macos == 'true' && (inputs.ios == 'true' && ', macOS' || ' macOS') || '' }}${{ inputs.android == 'true' && format('{0} Android{1}', (inputs.ios == 'true' || inputs.macos == 'true') && ',' || '', inputs.android_arch != '' && format(' ({0})', inputs.android_arch) || '') || '' }}${{ inputs.windows == 'true' && ((inputs.ios == 'true' || inputs.macos == 'true' || inputs.android == 'true') && ', Windows' || ' Windows') || '' }} + +permissions: + contents: write + +on: + workflow_dispatch: + inputs: + version: + description: Release version/tag to create + required: true + prerelease: + description: Mark the GitHub release as a pre-release + type: boolean + default: false + release_notes: + description: Optional release notes markdown. Empty uses a one-line fallback. + required: false + default: "" + webrtc_ref: + description: Branch, tag, or SHA for this repo (GetStream/webrtc) + required: true + default: main + ios: + description: Include iOS xcframework + type: boolean + default: true + macos: + description: Include macOS xcframework + type: boolean + default: true + android: + description: Include Android AAR + type: boolean + default: true + windows: + description: Include Windows libs (fails clearly if the runner is not a WebRTC host) + type: boolean + default: false + config: + description: GN configuration + type: choice + options: + - release + - debug + default: release + android_arch: + description: Optional Android ABI, for example arm64-v8a. Empty = default ABI set. + required: false + default: "" + +jobs: + release: + name: Release v2 + uses: ./.github/workflows/_make.yml + secrets: inherit + with: + mode: release + webrtc_ref: ${{ inputs.webrtc_ref }} + ios: ${{ inputs.ios }} + macos: ${{ inputs.macos }} + android: ${{ inputs.android }} + windows: ${{ inputs.windows }} + config: ${{ inputs.config }} + android_arch: ${{ inputs.android_arch }} + version: ${{ inputs.version }} + prerelease: ${{ inputs.prerelease }} + release_notes: ${{ inputs.release_notes }} diff --git a/.github/workflows/test-v2.yml b/.github/workflows/test-v2.yml new file mode 100644 index 0000000000..80a7a1f5d1 --- /dev/null +++ b/.github/workflows/test-v2.yml @@ -0,0 +1,45 @@ +# WebRTC tests (v2 Makefile DAG). Not dispatchable until this file exists +# on the default branch. _make.yml: HIT build-ios / build-macos, make test +# only (no extra framework-slice make build). No Linux Deps job. + +name: Test v2 + +run-name: >- + Test v2 config:debug${{ inputs.ios == 'true' && ' iOS' || '' }}${{ inputs.macos == 'true' && (inputs.ios == 'true' && ', macOS' || ' macOS') || '' }}${{ inputs.windows == 'true' && ((inputs.ios == 'true' || inputs.macos == 'true') && ', Windows' || ' Windows') || '' }} + +permissions: + contents: read + +on: + workflow_dispatch: + inputs: + webrtc_ref: + description: Branch, tag, or SHA for this repo (GetStream/webrtc) + required: true + default: main + ios: + description: Run iOS simulator tests + type: boolean + default: true + macos: + description: Run macOS host tests + type: boolean + default: true + windows: + description: Run Windows tests (fails clearly if the runner is not a WebRTC host) + type: boolean + default: false + +jobs: + test: + name: Test v2 + uses: ./.github/workflows/_make.yml + secrets: inherit + with: + mode: test + webrtc_ref: ${{ inputs.webrtc_ref }} + ios: ${{ inputs.ios }} + macos: ${{ inputs.macos }} + android: false + windows: ${{ inputs.windows }} + config: debug diff --git a/.gitignore b/.gitignore index 9b7a2aa225..3e45fbb3f2 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,7 @@ out_ios_libs .output .products out_macos_libs +# Leftover in-repo gclient parent (pre-webrtc/src bootstrap). +.gclient_deps/ +.gclient-git-cache +.idea diff --git a/stream_build/AGENTS.md b/stream_build/AGENTS.md new file mode 100644 index 0000000000..f2d12d7488 --- /dev/null +++ b/stream_build/AGENTS.md @@ -0,0 +1,200 @@ +# WebRTC Makefile wrapper + +Public API: + +``` +cd stream_build +make bootstrap # once; CONFIRM=1 in CI +make build|test|package ios|android|macos|windows [VAR=value ...] +make combine +make rename apple|android +``` + +`build` is gn+ninja. `package` only copies/lipo/zips artifacts already in `OUT`, then writes `LICENSE.md` unless `SKIP_LICENSES=1`. +Do not reintroduce Fastlane or wrap `tools_webrtc/ios/build_ios_libs.py`. + +Apple package writes per-platform trees so they do not overwrite: + +``` +$(PRODUCTS)/ios/WebRTC.xcframework +$(PRODUCTS)/macos/WebRTC.xcframework +$(PRODUCTS)/WebRTC.xcframework # make combine +$(PRODUCTS)/renamed/StreamWebRTC.xcframework +$(PRODUCTS)/renamed/libwebrtc.aar +``` + +`make combine` globs `$(PRODUCTS)/*/WebRTC.xcframework` (ios, macos, and any +future sibling such as visionos/tvos). One match is copied to the stable +output path; two or more are merged with `xcodebuild -create-xcframework`. + +`make rename` copies the original artifact and rebrands the copy. The +GetStream/webrtc release keeps `WebRTC.xcframework` / `libwebrtc.aar`. +Renamed copies feed stream-video-swift-webrtc and stream-video-android-webrtc. + +## Layout + +- `Makefile` — verb + platform dispatch +- `gn/common.args` — Stream policy GN args +- `gn/slices.tsv` — slice → ninja target + GN overlay +- `scripts/bootstrap.sh` — wrap this checkout as Chromium `webrtc/src` +- `webrtc.mk` — catch-all parent `webrtc/Makefile` template (copied if missing) +- `scripts/deps.sh` — `gclient sync` at `DEPS_ROOT`; uses this `src` (no second clone) +- `scripts/gn-gen.sh` — args.gn + gn gen +- `scripts/package-apple.sh` — lipo + create-xcframework +- `scripts/combine-apple.sh` — discover platform xcframeworks and merge +- `scripts/rename-apple.sh` — copy WebRTC.xcframework → StreamWebRTC +- `scripts/rename-android.sh` — copy libwebrtc.aar into PRODUCTS/renamed/ +- `scripts/package-android.sh` — zip libwebrtc.aar +- `scripts/package-windows.sh` — copy Windows libs +- `scripts/run-ios-tests.sh` +- `scripts/check.sh` + +Required tree (gclient parent **must** be named `webrtc`, checkout **must** +be named `src`): + +``` +webrtc/ # DEPS_ROOT / gclient root + Makefile # bootstrap copies webrtc.mk if missing + .gclient + .gclient-git-cache/ # GIT_CACHE_PATH + src/ # this git checkout (WEBRTC_SRC) + DEPS + stream_build/ + third_party/ # gclient writes here + out/ # ninja (sibling of src) +``` + +`solutions.name = src`, `managed: False`. `src` is this worktree, not a +symlink and not a second clone. Official DEPS stays (`src/build`, +`src/third_party`, `gclient_gn_args_file = src/build/config/gclient_args.gni`). + +`make bootstrap` (interactive; `CONFIRM=1` in CI) renames/wraps into that +layout. Other verbs +(except `help` / `check` / `bootstrap`) go through +`scripts/bootstrap.sh --check` and fail with `run: make bootstrap` unless +the tree is `webrtc/src` (real directory, not a symlink). `deps` / +`build` / `test` / `package` / `runhooks` also require parent `.gclient` +(`--check --gclient`). `make bootstrap` copies `webrtc.mk` to +`$(DEPS_ROOT)/Makefile` if that file is missing (parent is outside git). + +CI checks out with `path: src` so `GITHUB_WORKSPACE` is the webrtc-named +folder. `DEPS_ROOT=$GITHUB_WORKSPACE`. `OUT` is `$DEPS_ROOT/out` +(sibling of `src`). There is no shared Linux Deps job. iOS, macOS, +and Android jobs run in parallel after Plan: + +1. `actions/checkout` `src` at `webrtc_ref` +2. HIT Hetzner `build-ios` / `build-macos` / `build-android` (`if_missing: + skip`; Build dispatch `skip_deps_cache` skips the download) +3. `make deps` (`RUN_HOOKS=1`, host GCS rust-toolchain on Apple) +4. `make build` / `make test` (`SKIP_DEPS=1`) +5. `artifact-put` `.gclient-git-cache` plus reusable `src/resources` + and `.cipd` if non-empty (always after miss / `skip_deps_cache`; + skip PUT when HIT size delta is < 1GiB) + +Keys: `artifacts//build-{ios,macos,android}.tar`. +Same-OS only (Linux tree on Mac is forbidden). Members: required +`.gclient-git-cache`; `src/resources` and `.cipd` if non-empty. +Not packed: working trees (`src/third_party`, `src/build`, +`src/buildtools`, `src/testing`, `src/tools`, `src/ios`), `out/`, +`.gclient` / `.gclient_entries` (setup-webrtc writes `.gclient` every +job), `src/.git`, Stream-tracked `src` files, `products/`. Restore: +checkout `src`, extract git-cache to `GIT_CACHE_PATH` +(`${{ github.workspace }}/.gclient-git-cache`), `src/resources`, and +`.cipd` if present. Do not strip git alternates; `make deps` runs +`rewrite_git_cache_alternates` so Linux-packed cache paths retarget +to this runner. Always `make deps` after HIT (cheap from cache). +Build `CONFIG` is dispatch (default release); `make test` always uses debug +in `out/ios_tests` / `out/webrtc_tests`, so those subdirs do not mix +with slice dirs. test-v2.yml HITs the same `build-ios` / `build-macos` +keys. Windows Deps still uploads `deps-windows` to GitHub. +Package/Release Build jobs also `make package` and upload `products-*` +(GitHub). Package combine consumes `products-*` (no third ninja) and +uploads `final-*`. Release attaches `final-*`. `TARGET_OS` is the +platform of that job (`ios`, `mac`, `android,unix`). + +Hetzner: `artifact-download` Range-GETs with +`s3api get-object --range bytes=${have}-` into a partial file and +resumes from bytes already on disk (not `s3 cp` from 0). +While GET runs, logs `have / ContentLength (%)` every ~15s. +`artifact-put` tars to a file then multipart `aws s3 cp` to +`/artifacts//.tar` with +`--endpoint-url https://hel1.your-objectstorage.com` and region +`hel1`. A pipe GET/PUT is one HTTP body; a drop is IncompleteRead +of the whole object. Peak disk is tar + tree (~2x); delete the tar +after extract/upload. `aws s3 cp` / `s3api` talk to Hetzner's +S3-compatible API, not AWS. +Callers pass org secrets via `with:` +`${{ secrets.HETZNER_ACCESS_KEY_CI_ARTIFACTS }}`, +`${{ secrets.HETZNER_SECRET_ACCESS_KEY_CI_ARTIFACTS }}`, and +`${{ secrets.HETZNER_BUCKET_CI_ARTIFACTS }}`. Composite actions must +not use `${{ secrets.* }}`. `products-*` / `final-*` stay on +`actions/upload-artifact`. + +## Host gates + +- bootstrap: any host (writes layout + `.gclient`; no depot_tools) +- ios / macos / combine / rename apple: Darwin +- android / rename android: Linux for build/package; rename android is a file copy on any host +- windows: Windows +- deps / runhooks: any host with depot_tools + +## Overrides + +| Name | Role | +|------|------| +| `CONFIG` | `release` (default, `is_debug=false`) or `debug`. `make test` always uses debug. | +| `GN_ARGS` | extra `key=value` tokens, applied last | +| `DEPS_ROOT` | gclient parent (`webrtc/`; `.gclient` + `src/` + `out/`) | +| `WEBRTC_SRC` | this git checkout (`webrtc/src`; default: parent of `stream_build/`) | +| `OUT` / `PRODUCTS` | ninja dirs / packaged output (default under `DEPS_ROOT`, sibling of `src`) | +| `GIT_CACHE_PATH` | gclient object cache (default `DEPS_ROOT/.gclient-git-cache`) | +| `ARCHS` | android ABI or windows cpu (`arm64-v8a`, `x64`, …) | +| `JOBS` | ninja/gclient parallelism | +| `SHALLOW` | `1` (default) `gclient sync --no-history --shallow`. `0` = full history. | +| `ZIP` | `1` to zip Apple/Windows products | +| `XCFRAMEWORK` | input for `make rename apple` (default `$(PRODUCTS)/WebRTC.xcframework`) | +| `AAR` | input for `make rename android` (default `$(PRODUCTS)/libwebrtc.aar`) | +| `RENAMED` | output dir for `make rename` (default `$(PRODUCTS)/renamed`) | +| `SKIP_DEPS` | `1` skips gclient sync only; build still runs. Default `0`. | +| `SKIP_LICENSES` | `1` skips `LICENSE.md` generation only; lipo/zip still run. Default `0`. | +| `SKIP_MACCATALYST` | `1` drops `catalyst-arm64` and `catalyst-x64` from ios build+package only. Default `0`. | + +```bash +make bootstrap CONFIRM=1 +make build ios +make build ios SKIP_DEPS=1 +make build ios SKIP_MACCATALYST=1 +make build android ARCHS=arm64-v8a SKIP_DEPS=1 +make package ios SKIP_DEPS=1 SKIP_LICENSES=1 +make package macos SKIP_DEPS=1 SKIP_LICENSES=1 +make combine SKIP_LICENSES=1 +make rename apple +make rename android +``` + +## GitHub Actions DAGs + +Two independent dispatch DAGs. Either can `workflow_dispatch` without +the other. + +**v2 (Makefile / this tree)** + +| UI name | File | +|---|---| +| Build v2 | `.github/workflows/build-v2.yml` | +| Test v2 | `.github/workflows/test-v2.yml` | +| Package v2 | `.github/workflows/package-v2.yml` | +| Release v2 | `.github/workflows/release-v2.yml` | + +Reusable: `.github/workflows/_make.yml` (`name: WebRTC make`). +Actions: `restore-tree`, `artifact-put`, `artifact-download`, +`setup-webrtc`, `prepare-common-v2`. + +**main (legacy Fastlane / stream-webrtc-release-pipeline)** + +| UI name | File | +|---|---| +| Build | `.github/workflows/manual-platform-tests.yml` | +| Publish | `.github/workflows/publish.yml` | + +Actions: `prepare-common`, `prepare-apple`, `prepare-android`. diff --git a/stream_build/Makefile b/stream_build/Makefile new file mode 100644 index 0000000000..739ef83c09 --- /dev/null +++ b/stream_build/Makefile @@ -0,0 +1,379 @@ +# Public API: +# make bootstrap +# make build|test|package ios|android|macos|windows [VAR=value ...] +# make combine +# make rename apple|android +# GN overrides: CONFIG=debug GN_ARGS='key=value key2=value' +# +# Layout: Chromium-style webrtc/src (this git checkout). Run make bootstrap +# once. Other verbs assume that layout. +# From the git root: cd stream_build && make build ios +# From webrtc/: make build ios (bootstrap writes parent Makefile) +# SKIP_DEPS=1 skips gclient sync only; gn/ninja still run. +# SKIP_LICENSES=1 skips license generation only; lipo/zip still run. +# SKIP_MACCATALYST=1 drops catalyst-* from ios build+package only. +# package ios/macos write PRODUCTS/ios and PRODUCTS/macos. +# make combine merges whatever PRODUCTS/*/WebRTC.xcframework exist. +# make rename copies WebRTC.xcframework / libwebrtc.aar to PRODUCTS/renamed/. + +SHELL := /bin/bash +.SUFFIXES: + +PIPELINE := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +SCRIPTS := $(PIPELINE)scripts + +CONFIG ?= release +TARGET_OS ?= ios +JOBS ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 8) +SHALLOW ?= 1 +SKIP_DEPS ?= 0 +SKIP_LICENSES ?= 0 +SKIP_MACCATALYST ?= 0 +RUN_HOOKS ?= 1 +CONFIRM ?= 0 +GN_ARGS ?= +EXTRA_GN_ARGS ?= +WEBRTC_REPO ?= git@github.com:GetStream/webrtc.git +WEBRTC_ROOT ?= +REPO_ROOT := $(abspath $(PIPELINE)/..) +WEBRTC_SRC ?= $(REPO_ROOT) +ifneq ($(WEBRTC_ROOT),) +DEPS_ROOT ?= $(WEBRTC_ROOT) +endif +DEPS_ROOT ?= $(abspath $(REPO_ROOT)/..) +OUT ?= $(DEPS_ROOT)/out +PRODUCTS ?= $(DEPS_ROOT)/products +GIT_CACHE_PATH ?= $(DEPS_ROOT)/.gclient-git-cache +TARGET ?= +NINJA_TARGET ?= +ARCHS ?= +TEST_TARGETS ?= +ZIP ?= 0 +XCFRAMEWORK ?= $(PRODUCTS)/WebRTC.xcframework +AAR ?= $(PRODUCTS)/libwebrtc.aar +RENAMED ?= $(PRODUCTS)/renamed +SIMULATOR_PLATFORM ?= +SIMULATOR_VERSION ?= +EXTRA_ARGS ?= + +ALL_GN_ARGS := $(strip $(EXTRA_GN_ARGS) $(GN_ARGS)) + +CATALYST_SLICES := catalyst-arm64 catalyst-x64 +IOS_SLICES := ios-arm64-device ios-arm64-simulator ios-x64-simulator $(CATALYST_SLICES) +MACOS_SLICES := macos-arm64 macos-x64 +ANDROID_SLICES := android-armeabi-v7a android-arm64-v8a android-x86 android-x86_64 +WINDOWS_SLICES := windows-x64 windows-arm64 +IOS_TEST_TARGETS ?= sdk_unittests sdk_framework_unittests +MACOS_TEST_TARGETS ?= rtc_unittests rtc_pc_unittests rtc_stats_unittests +WINDOWS_TEST_TARGETS ?= rtc_unittests rtc_pc_unittests rtc_stats_unittests + +ios_slices = $(if $(filter 1,$(SKIP_MACCATALYST)),$(filter-out $(CATALYST_SLICES),$(IOS_SLICES)),$(IOS_SLICES)) +android_slices = $(if $(ARCHS),$(addprefix android-,$(ARCHS)),$(ANDROID_SLICES)) +windows_slices = $(if $(ARCHS),$(addprefix windows-,$(ARCHS)),$(WINDOWS_SLICES)) + +PLATFORMS := ios android macos windows +VERBS := build test package +PLATFORM := $(firstword $(filter $(PLATFORMS),$(MAKECMDGOALS))) + +export DEPS_ROOT WEBRTC_SRC WEBRTC_ROOT WEBRTC_REPO WEBRTC_REVISION WEBRTC_REF +export TARGET_OS JOBS SHALLOW RUN_HOOKS CONFIG OUT PRODUCTS ARCHS SKIP_LICENSES +export GN_ARGS EXTRA_GN_ARGS ALL_GN_ARGS GIT_CACHE_PATH CONFIRM BOOTSTRAP_SRC + +.DEFAULT_GOAL := help + +.PHONY: help check bootstrap deps runhooks gen ninja slice clean print-gn-args \ + announce require-layout require-gclient require-src require-darwin \ + require-linux require-windows maybe-deps \ + combine apple rename rename-apple rename-android \ + $(PLATFORMS) $(VERBS) \ + build-ios build-macos build-android build-windows \ + test-ios test-macos test-android test-windows \ + package-ios package-macos package-android package-windows \ + $(addprefix build-slice-,$(IOS_SLICES) $(MACOS_SLICES) $(ANDROID_SLICES) $(WINDOWS_SLICES)) + +ifneq ($(filter build,$(MAKECMDGOALS)),) + ifneq ($(filter package,$(MAKECMDGOALS)),) + package: build + endif +endif + +help: + @echo "make bootstrap" + @echo "make build|test|package ios|android|macos|windows [VAR=value ...]" + @echo "make combine" + @echo "make rename apple|android" + @echo + @echo " bootstrap wrap this checkout as webrtc/src (CONFIRM=1 in CI)" + @echo " build gn gen + ninja" + @echo " test build and run tests" + @echo " package assemble artifacts (xcframework, aar, zip)" + @echo " combine merge PRODUCTS/*/WebRTC.xcframework into one xcframework" + @echo " rename copy+rebrand for wrapper SDKs (original untouched)" + @echo + @echo " CONFIG=release (default, is_debug=false) or CONFIG=debug" + @echo " tests always gn-gen with debug, ignoring CONFIG" + @echo " SHALLOW=1 (default) gclient --no-history --shallow; SHALLOW=0 full history" + @echo " SKIP_DEPS=1 skips gclient sync only; build/test/package still run" + @echo " SKIP_LICENSES=1 skips license generation only; lipo/zip still run" + @echo " SKIP_MACCATALYST=1 drops catalyst-arm64 catalyst-x64 from ios" + @echo + @echo " make package ios" + @echo " make package macos" + @echo " from webrtc/: make build ios" + @echo " make combine # merges whatever platform dirs exist under PRODUCTS" + @echo " make rename apple XCFRAMEWORK=path/to/WebRTC.xcframework" + @echo " make rename android AAR=path/to/libwebrtc.aar" + @echo " make build package ios compile then pack" + @echo " make build android ARCHS=arm64-v8a" + @echo " make build ios GN_ARGS='rtc_use_h264=false' CONFIG=debug" + @echo " make bootstrap CONFIRM=1" + @echo " make build ios SKIP_MACCATALYST=1" + @echo + @echo "Also: deps runhooks check clean print-gn-args TARGET=slice" + @echo "Vars: DEPS_ROOT WEBRTC_SRC OUT PRODUCTS GIT_CACHE_PATH CONFIG GN_ARGS JOBS SHALLOW ARCHS ZIP XCFRAMEWORK AAR RENAMED SKIP_DEPS SKIP_LICENSES SKIP_MACCATALYST CONFIRM" + @echo "OUT defaults to DEPS_ROOT/out (sibling of src)." + +check: + @$(SCRIPTS)/check.sh + +bootstrap: + @$(SCRIPTS)/bootstrap.sh + +deps: require-gclient + @$(SCRIPTS)/deps.sh sync + +runhooks: require-gclient + @$(SCRIPTS)/deps.sh runhooks + +print-gn-args: require-layout + @test -n "$(TARGET)" || { echo "TARGET is required (slice name)"; exit 1; } + @$(SCRIPTS)/gn-gen.sh --print --slice "$(TARGET)" --config "$(CONFIG)" --extra "$(ALL_GN_ARGS)" + +require-layout: + @$(SCRIPTS)/bootstrap.sh --check + +require-gclient: + @$(SCRIPTS)/bootstrap.sh --check --gclient + +require-src: require-layout + @test -n "$(WEBRTC_SRC)" || { echo "run: make bootstrap"; exit 1; } + @test ! -L "$(WEBRTC_SRC)" || { echo "run: make bootstrap"; exit 1; } + @test -f "$(WEBRTC_SRC)/DEPS" || { echo "run: make bootstrap"; exit 1; } + +require-darwin: + @test "$$(uname -s)" = Darwin || { echo "Apple targets require macOS"; exit 1; } + +require-linux: + @test "$$(uname -s)" = Linux || { echo "Android builds require Linux"; exit 1; } + +require-windows: + @case "$$(uname -s)" in MINGW*|MSYS*|CYGWIN*) exit 0 ;; esac; \ + test "$${OS}" = Windows_NT || { echo "Windows targets require Windows"; exit 1; } + +maybe-deps: require-gclient +ifneq ($(SKIP_DEPS),1) + $(MAKE) deps +endif + +$(PLATFORMS) apple: + @: + +announce: + @effective="$(CONFIG)"; \ + note=""; \ + if [[ "$(VERB)" == test ]]; then \ + effective=debug; \ + note=" (tests always debug)"; \ + fi; \ + echo "==> $(strip $(VERB) $(PLATFORM))"; \ + echo " config: $$effective$$note"; \ + echo " deps_root: $(DEPS_ROOT)"; \ + echo " src: $(WEBRTC_SRC)"; \ + echo " out: $(OUT)"; \ + products="$(PRODUCTS)"; \ + case "$(PLATFORM)" in \ + ios) products="$(PRODUCTS)/ios" ;; \ + macos) products="$(PRODUCTS)/macos" ;; \ + esac; \ + echo " products: $$products"; \ + echo " jobs: $(JOBS)"; \ + echo " shallow: $(SHALLOW)"; \ + echo " skip_deps: $(SKIP_DEPS)"; \ + echo " skip_licenses: $(SKIP_LICENSES)"; \ + echo " skip_maccatalyst: $(SKIP_MACCATALYST)"; \ + if [[ -n "$(ALL_GN_ARGS)" ]]; then echo " gn_args: $(ALL_GN_ARGS)"; fi; \ + case "$(PLATFORM)" in \ + ios) echo " slices: $(ios_slices)" ;; \ + macos) echo " slices: $(MACOS_SLICES)" ;; \ + android) \ + if [[ "$(VERB)" != rename ]]; then echo " slices: $(android_slices)"; fi ;; \ + windows) echo " slices: $(windows_slices)" ;; \ + esac; \ + if [[ "$(VERB)" == test ]]; then \ + case "$(PLATFORM)" in \ + ios) echo " tests: $(if $(TEST_TARGETS),$(TEST_TARGETS),$(IOS_TEST_TARGETS))" ;; \ + macos) echo " tests: $(if $(TEST_TARGETS),$(TEST_TARGETS),$(MACOS_TEST_TARGETS))" ;; \ + windows) echo " tests: $(if $(TEST_TARGETS),$(TEST_TARGETS),$(WINDOWS_TEST_TARGETS))" ;; \ + esac; \ + fi; \ + if [[ "$(VERB)" == rename ]]; then \ + case "$(PLATFORM)" in \ + apple) \ + echo " input: $(XCFRAMEWORK)"; \ + echo " output: $(RENAMED)/StreamWebRTC.xcframework" ;; \ + android) \ + echo " input: $(AAR)"; \ + echo " output: $(RENAMED)/libwebrtc.aar" ;; \ + esac; \ + fi; \ + if [[ "$$(uname -s)" == Darwin ]]; then \ + echo " xcode: $$(xcodebuild -version 2>/dev/null | paste -sd ' ' -)"; \ + echo " developer: $${DEVELOPER_DIR:-$$(xcode-select -p 2>/dev/null)}"; \ + fi + +build test package: require-gclient + @test -n "$(PLATFORM)" || { echo "usage: make $@ ios|android|macos|windows"; exit 1; } + @$(MAKE) --no-print-directory announce VERB=$@ PLATFORM="$(PLATFORM)" + @$(MAKE) --no-print-directory $@-$(PLATFORM) + +combine: require-darwin require-src + @$(MAKE) --no-print-directory announce VERB=combine PLATFORM= + @$(SCRIPTS)/combine-apple.sh \ + --src "$(WEBRTC_SRC)" \ + --out "$(OUT)" \ + --products "$(PRODUCTS)" \ + $(if $(filter 1,$(ZIP)),--zip,) + +rename: require-layout + @target="$(firstword $(filter apple android,$(MAKECMDGOALS)))"; \ + test -n "$$target" || { echo "usage: make rename apple|android"; exit 1; }; \ + $(MAKE) --no-print-directory announce VERB=rename PLATFORM="$$target"; \ + $(MAKE) --no-print-directory rename-$$target + +rename-apple: require-darwin + @$(SCRIPTS)/rename-apple.sh \ + --src "$(XCFRAMEWORK)" \ + --dest "$(RENAMED)" + +rename-android: + @$(SCRIPTS)/rename-android.sh \ + --src "$(AAR)" \ + --dest "$(RENAMED)" + +gen: require-src + @test -n "$(TARGET)" || { echo "TARGET is required (slice name)"; exit 1; } + @$(SCRIPTS)/gn-gen.sh \ + --src "$(WEBRTC_SRC)" \ + --out "$(OUT)/$(TARGET)" \ + --slice "$(TARGET)" \ + --config "$(CONFIG)" \ + --extra "$(ALL_GN_ARGS)" + +ninja: require-src + @test -n "$(TARGET)" || { echo "TARGET is required (slice name)"; exit 1; } + @ninja_bin="$$($(SCRIPTS)/common.sh ninja "$(WEBRTC_SRC)")"; \ + ninja_target="$(NINJA_TARGET)"; \ + if [[ -z "$$ninja_target" ]]; then \ + ninja_target="$$($(SCRIPTS)/gn-gen.sh --ninja-target "$(TARGET)")"; \ + fi; \ + echo "$$ninja_bin -C $(OUT)/$(TARGET) $$ninja_target"; \ + "$$ninja_bin" -C "$(OUT)/$(TARGET)" $$ninja_target -j"$(JOBS)" + +slice: gen ninja + +$(addprefix build-slice-,$(IOS_SLICES) $(MACOS_SLICES) $(ANDROID_SLICES) $(WINDOWS_SLICES)): build-slice-%: require-src + @$(MAKE) --no-print-directory slice TARGET=$* + +build-ios: require-darwin maybe-deps require-src $(addprefix build-slice-,$(ios_slices)) +build-macos: require-darwin maybe-deps require-src $(addprefix build-slice-,$(MACOS_SLICES)) +build-android: require-linux maybe-deps require-src $(addprefix build-slice-,$(android_slices)) +build-windows: require-windows maybe-deps require-src $(addprefix build-slice-,$(windows_slices)) + +package-ios: require-darwin require-src + @$(SCRIPTS)/package-apple.sh \ + --src "$(WEBRTC_SRC)" \ + --out "$(OUT)" \ + --products "$(PRODUCTS)/ios" \ + --slices "$(ios_slices)" \ + $(if $(filter 1,$(ZIP)),--zip,) + +package-macos: require-darwin require-src + @$(SCRIPTS)/package-apple.sh \ + --src "$(WEBRTC_SRC)" \ + --out "$(OUT)" \ + --products "$(PRODUCTS)/macos" \ + --slices "$(MACOS_SLICES)" \ + $(if $(filter 1,$(ZIP)),--zip,) + +package-android: require-linux require-src + @$(SCRIPTS)/package-android.sh \ + --src "$(WEBRTC_SRC)" \ + --out "$(OUT)" \ + --products "$(PRODUCTS)" \ + --slices "$(android_slices)" + +package-windows: require-windows require-src + @$(SCRIPTS)/package-windows.sh \ + --out "$(OUT)" \ + --products "$(PRODUCTS)" \ + --slices "$(windows_slices)" \ + $(if $(filter 1,$(ZIP)),--zip,) + +test-ios: require-darwin maybe-deps require-src + @$(SCRIPTS)/gn-gen.sh \ + --src "$(WEBRTC_SRC)" \ + --out "$(OUT)/ios_tests" \ + --config debug \ + --overlay ios-test \ + --extra "$(ALL_GN_ARGS)" + @ninja_bin="$$($(SCRIPTS)/common.sh ninja "$(WEBRTC_SRC)")"; \ + targets="$(if $(TEST_TARGETS),$(TEST_TARGETS),$(IOS_TEST_TARGETS))"; \ + echo "$$ninja_bin -C $(OUT)/ios_tests $$targets"; \ + "$$ninja_bin" -C "$(OUT)/ios_tests" $$targets -j"$(JOBS)" + @$(SCRIPTS)/run-ios-tests.sh \ + --build-dir "$(OUT)/ios_tests" \ + --targets "$(if $(TEST_TARGETS),$(TEST_TARGETS),$(IOS_TEST_TARGETS))" \ + $(if $(SIMULATOR_PLATFORM),--platform "$(SIMULATOR_PLATFORM)",) \ + $(if $(SIMULATOR_VERSION),--version "$(SIMULATOR_VERSION)",) \ + $(if $(EXTRA_ARGS),--extra "$(EXTRA_ARGS)",) + +test-macos: require-darwin maybe-deps require-src + @$(SCRIPTS)/gn-gen.sh \ + --src "$(WEBRTC_SRC)" \ + --out "$(OUT)/webrtc_tests" \ + --config debug \ + --overlay macos-test \ + --extra "$(ALL_GN_ARGS)" + @ninja_bin="$$($(SCRIPTS)/common.sh ninja "$(WEBRTC_SRC)")"; \ + targets="$(if $(TEST_TARGETS),$(TEST_TARGETS),$(MACOS_TEST_TARGETS))"; \ + echo "$$ninja_bin -C $(OUT)/webrtc_tests $$targets"; \ + "$$ninja_bin" -C "$(OUT)/webrtc_tests" $$targets -j"$(JOBS)"; \ + filter="$$(tr -d '\n' < "$(SCRIPTS)/macos-gtest-filter.txt")"; \ + for target in $$targets; do \ + echo "$(OUT)/webrtc_tests/$$target"; \ + "$(OUT)/webrtc_tests/$$target" --gtest_filter="-$$filter" $(EXTRA_ARGS); \ + done + +test-android: + @echo "error: make test android is not wired (no device runner)." >&2 + @echo "use: make build android && make package android" >&2 + @exit 1 + +test-windows: require-windows maybe-deps require-src + @$(SCRIPTS)/gn-gen.sh \ + --src "$(WEBRTC_SRC)" \ + --out "$(OUT)/windows_tests" \ + --config debug \ + --overlay windows-test \ + --extra "$(ALL_GN_ARGS)" + @ninja_bin="$$($(SCRIPTS)/common.sh ninja "$(WEBRTC_SRC)")"; \ + targets="$(if $(TEST_TARGETS),$(TEST_TARGETS),$(WINDOWS_TEST_TARGETS))"; \ + echo "$$ninja_bin -C $(OUT)/windows_tests $$targets"; \ + "$$ninja_bin" -C "$(OUT)/windows_tests" $$targets -j"$(JOBS)"; \ + for target in $$targets; do \ + echo "$(OUT)/windows_tests/$$target"; \ + "$(OUT)/windows_tests/$$target" $(EXTRA_ARGS); \ + done + +clean: require-layout + rm -rf "$(OUT)" "$(PRODUCTS)" diff --git a/stream_build/gn/android.args b/stream_build/gn/android.args new file mode 100644 index 0000000000..8ffc5dc8f0 --- /dev/null +++ b/stream_build/gn/android.args @@ -0,0 +1 @@ +android_static_analysis = "off" diff --git a/stream_build/gn/apple.args b/stream_build/gn/apple.args new file mode 100644 index 0000000000..8475612194 --- /dev/null +++ b/stream_build/gn/apple.args @@ -0,0 +1,7 @@ +# Overlay for Apple framework slices (iOS, Catalyst, macOS). +rtc_enable_objc_symbol_export = true +ios_enable_code_signing = false +enable_dsyms = true +enable_stripping = true +rtc_libvpx_build_vp9 = true +use_rtti = false diff --git a/stream_build/gn/common.args b/stream_build/gn/common.args new file mode 100644 index 0000000000..6ae84c674a --- /dev/null +++ b/stream_build/gn/common.args @@ -0,0 +1,10 @@ +# Stream policy defaults applied to every gn gen. +rtc_allow_deprecated_namespaces = true +stream_enable_rendering_backend = true +is_component_build = false +rtc_include_tests = false +rtc_build_examples = false +treat_warnings_as_errors = false +use_siso = false +use_remoteexec = false +use_reclient = false diff --git a/stream_build/gn/ios-test.args b/stream_build/gn/ios-test.args new file mode 100644 index 0000000000..90e4e665eb --- /dev/null +++ b/stream_build/gn/ios-test.args @@ -0,0 +1,11 @@ +target_os = "ios" +target_environment = "simulator" +target_cpu = "arm64" +ios_enable_code_signing = false +ios_deployment_target = "13.0" +rtc_include_tests = true +enable_run_ios_unittests_with_xctest = true +is_debug = true +use_siso = false +use_remoteexec = false +use_reclient = false diff --git a/stream_build/gn/macos-test.args b/stream_build/gn/macos-test.args new file mode 100644 index 0000000000..8e90e8f672 --- /dev/null +++ b/stream_build/gn/macos-test.args @@ -0,0 +1,6 @@ +target_os = "mac" +rtc_include_tests = true +is_debug = true +use_siso = false +use_remoteexec = false +use_reclient = false diff --git a/stream_build/gn/slices.tsv b/stream_build/gn/slices.tsv new file mode 100644 index 0000000000..2610d1c344 --- /dev/null +++ b/stream_build/gn/slices.tsv @@ -0,0 +1,14 @@ +# name ninja_target gn_args (space-separated key=value) +ios-arm64-device framework_objc target_os="ios" target_environment="device" target_cpu="arm64" ios_deployment_target="13.0" +ios-arm64-simulator framework_objc target_os="ios" target_environment="simulator" target_cpu="arm64" ios_deployment_target="13.0" +ios-x64-simulator framework_objc target_os="ios" target_environment="simulator" target_cpu="x64" ios_deployment_target="13.0" +catalyst-arm64 framework_objc target_os="ios" target_environment="catalyst" target_cpu="arm64" ios_deployment_target="14.0" use_lld=false +catalyst-x64 framework_objc target_os="ios" target_environment="catalyst" target_cpu="x64" ios_deployment_target="14.0" use_lld=false +macos-arm64 mac_framework_objc target_os="mac" target_cpu="arm64" +macos-x64 mac_framework_objc target_os="mac" target_cpu="x64" +android-armeabi-v7a sdk/android:libwebrtc sdk/android:libjingle_peerconnection_so target_os="android" target_cpu="arm" arm_version=7 +android-arm64-v8a sdk/android:libwebrtc sdk/android:libjingle_peerconnection_so target_os="android" target_cpu="arm64" +android-x86 sdk/android:libwebrtc sdk/android:libjingle_peerconnection_so target_os="android" target_cpu="x86" +android-x86_64 sdk/android:libwebrtc sdk/android:libjingle_peerconnection_so target_os="android" target_cpu="x64" +windows-x64 webrtc target_os="win" target_cpu="x64" +windows-arm64 webrtc target_os="win" target_cpu="arm64" diff --git a/stream_build/gn/windows-test.args b/stream_build/gn/windows-test.args new file mode 100644 index 0000000000..69ba285fde --- /dev/null +++ b/stream_build/gn/windows-test.args @@ -0,0 +1,6 @@ +target_os = "win" +rtc_include_tests = true +is_debug = true +use_siso = false +use_remoteexec = false +use_reclient = false diff --git a/stream_build/gn/windows.args b/stream_build/gn/windows.args new file mode 100644 index 0000000000..d11596de9e --- /dev/null +++ b/stream_build/gn/windows.args @@ -0,0 +1 @@ +# Overlay for Windows ninja slices. diff --git a/stream_build/scripts/bootstrap.sh b/stream_build/scripts/bootstrap.sh new file mode 100755 index 0000000000..2b1adf2b3b --- /dev/null +++ b/stream_build/scripts/bootstrap.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env bash +# Put this checkout at webrtc/src and write the parent .gclient. +# src is this worktree (no second clone). Refuses a symlink src. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +CONFIRM="${CONFIRM:-0}" + +usage() { + cat <<'EOF' +usage: bootstrap.sh [--check] [--gclient] + + make bootstrap interactive wrap to webrtc/src + make bootstrap CONFIRM=1 non-interactive (CI) + +env: + CONFIRM 1 to skip prompts + BOOTSTRAP_SRC git checkout to wrap (overrides WEBRTC_SRC) + WEBRTC_SRC git checkout (default: stream_build/..) + DEPS_ROOT gclient parent (default: parent of src) + WEBRTC_REPO written into .gclient + TARGET_OS written into .gclient +EOF +} + +logical_pwd() { + (cd "$1" && pwd) +} + +ask() { + local prompt="$1" + if [[ "$CONFIRM" == "1" ]]; then + echo "$prompt (CONFIRM=1: yes)" + return 0 + fi + if [[ ! -t 0 ]]; then + echo "error: non-interactive bootstrap requires CONFIRM=1" >&2 + return 1 + fi + local ans="" + read -r -p "$prompt [y/N] " ans || true + [[ "$ans" == "y" || "$ans" == "Y" || "$ans" == "yes" ]] +} + +refuse_with_commands() { + echo "error: refused. run:" >&2 + local cmd + for cmd in "$@"; do + echo " $cmd" >&2 + done + echo "then: make bootstrap" >&2 + exit 1 +} + +layout_fail() { + echo "run: make bootstrap" >&2 + return 1 +} + +# 1-3 always: basename src, parent webrtc, src is a real directory. +# --gclient also requires DEPS_ROOT/.gclient (deps/build/test/package). +layout_check() { + local src="$1" + local deps_root="$2" + local need_gclient="${3:-0}" + if [[ ! -d "$src" || -L "$src" ]]; then + layout_fail + return 1 + fi + if [[ "$(basename "$src")" != "src" ]]; then + layout_fail + return 1 + fi + if [[ "$(basename "$deps_root")" != "webrtc" ]]; then + layout_fail + return 1 + fi + if [[ ! -d "$deps_root/src" || -L "$deps_root/src" ]]; then + layout_fail + return 1 + fi + local src_real deps_src_real + src_real="$(cd "$src" && pwd -P)" + deps_src_real="$(cd "$deps_root/src" && pwd -P)" + if [[ "$src_real" != "$deps_src_real" ]]; then + layout_fail + return 1 + fi + if [[ "$need_gclient" == "1" && ! -f "$deps_root/.gclient" ]]; then + layout_fail + return 1 + fi + return 0 +} + +install_parent_makefile() { + local dest="$1/Makefile" + local tmpl="$SCRIPT_DIR/../webrtc.mk" + if [[ -e "$dest" ]]; then + echo "keeping $dest" + return 0 + fi + [[ -f "$tmpl" ]] || die "missing template $tmpl" + cp "$tmpl" "$dest" + echo "wrote $dest" +} + +finish_layout() { + local src="$1" + local deps_root="$2" + local git_cache="${GIT_CACHE_PATH:-$deps_root/.gclient-git-cache}" + mkdir -p "$git_cache" + write_gclient "$deps_root" + install_parent_makefile "$deps_root" + rewrite_git_cache_alternates "$src" "$git_cache" + echo "gclient root: $deps_root" + echo "src: $src" + echo "git-cache: $git_cache" + echo "out: $deps_root/out" +} + +cmd_check() { + local src deps_root need_gclient=0 arg + for arg in "$@"; do + case "$arg" in + --gclient) need_gclient=1 ;; + esac + done + src="${BOOTSTRAP_SRC:-${WEBRTC_SRC:-$(logical_pwd "$SCRIPT_DIR/..")}}" + deps_root="${DEPS_ROOT:-$(logical_pwd "$src/..")}" + layout_check "$src" "$deps_root" "$need_gclient" || exit 1 +} + +cmd_bootstrap() { + local src + src="${BOOTSTRAP_SRC:-${WEBRTC_SRC:-$(logical_pwd "$SCRIPT_DIR/..")}}" + [[ -d "$src" ]] || die "checkout not found: $src" + if [[ -L "$src" ]]; then + die "bootstrap refuses symlink src ($src -> $(readlink "$src"))" + fi + src="$(logical_pwd "$src")" + [[ -f "$src/DEPS" ]] || die "no DEPS at $src" + + if [[ "$(basename "$src")" != "src" ]]; then + local parent rename_dest + parent="$(dirname "$src")" + rename_dest="$parent/src" + echo "checkout is named '$(basename "$src")'; gclient requires 'src'." + echo "plan:" + echo " mv $src $rename_dest" + if [[ -e "$rename_dest" ]]; then + die "refusing to overwrite $rename_dest" + fi + if ! ask "rename to src?"; then + refuse_with_commands "mv $src $rename_dest" + fi + echo "mv $src $rename_dest" + mv "$src" "$rename_dest" + src="$rename_dest" + fi + + local parent deps_root + parent="$(dirname "$src")" + if [[ "$(basename "$parent")" != "webrtc" ]]; then + deps_root="$parent/webrtc" + echo "parent is named '$(basename "$parent")'; gclient root must be named webrtc." + echo "plan:" + echo " mkdir $deps_root" + echo " mv $src $deps_root/src" + echo "result: $deps_root/src" + if [[ -e "$deps_root" ]]; then + die "refusing to overwrite $deps_root" + fi + if ! ask "wrap as $deps_root/src?"; then + refuse_with_commands "mkdir $deps_root" "mv $src $deps_root/src" + fi + mkdir "$deps_root" + echo "mv $src $deps_root/src" + mv "$src" "$deps_root/src" + src="$deps_root/src" + parent="$deps_root" + fi + + deps_root="$(logical_pwd "$parent")" + src="$(logical_pwd "$src")" + if [[ -L "$src" || -L "$deps_root/src" ]]; then + die "bootstrap refuses symlink src" + fi + + finish_layout "$src" "$deps_root" +} + +case "${1:-}" in + --check) + shift + cmd_check "$@" + ;; + -h|--help|help) usage ;; + "") cmd_bootstrap ;; + *) usage >&2; exit 1 ;; +esac diff --git a/stream_build/scripts/check.sh b/stream_build/scripts/check.sh new file mode 100755 index 0000000000..cc93b6746e --- /dev/null +++ b/stream_build/scripts/check.sh @@ -0,0 +1,473 @@ +#!/usr/bin/env bash +# Sanity check for the Makefile wrapper (no real gclient tree required). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" +# make check exports WEBRTC_SRC/DEPS_ROOT. Fixtures must not inherit them. +unset WEBRTC_SRC DEPS_ROOT WEBRTC_ROOT BOOTSTRAP_SRC + +init_git_tree() { + local dir="$1" + mkdir -p "$dir" + printf 'hooks = []\n' > "$dir/DEPS" + git -C "$dir" init -q + git -C "$dir" config user.email "check@example.com" + git -C "$dir" config user.name "check" + git -C "$dir" config commit.gpgsign false + git -C "$dir" add DEPS + git -C "$dir" commit -q -m init +} + +fake_layout() { + local parent + parent="$(mktemp -d)/webrtc" + mkdir -p "$parent/src" + printf 'hooks = []\n' > "$parent/src/DEPS" + printf '%s\n' 'solutions = [{"name": "src", "managed": False}]' \ + > "$parent/.gclient" + printf '%s\n' "$parent" +} + +help_text="$(make -s help)" +[[ "$help_text" == *"make build|test|package"* ]] +printf '%s\n' "$help_text" | grep -q 'make bootstrap' +printf '%s\n' "$help_text" | grep -q 'CONFIG=release (default' +printf '%s\n' "$help_text" | grep -q 'make combine' +printf '%s\n' "$help_text" | grep -q 'make rename apple' +printf '%s\n' "$help_text" | grep -q 'SKIP_MACCATALYST=1' +printf '%s\n' "$help_text" | grep -q 'make package ios' +printf '%s\n' "$help_text" | grep -q 'make package macos' +! printf '%s\n' "$help_text" | grep -q 'package apple' + +layout="$(fake_layout)" +layout_make=(make DEPS_ROOT="$layout" WEBRTC_SRC="$layout/src") + +usage="$("${layout_make[@]}" build 2>&1 || true)" +printf '%s\n' "$usage" | grep -q 'usage: make build' +! printf '%s\n' "$usage" | grep -q '|apple' + +text="$("${layout_make[@]}" -s print-gn-args TARGET=ios-arm64-device CONFIG=release)" +printf '%s\n' "$text" | grep -q 'stream_enable_rendering_backend = true' +printf '%s\n' "$text" | grep -q 'target_os = "ios"' +printf '%s\n' "$text" | grep -q 'is_debug = false' + +debug="$("${layout_make[@]}" -s print-gn-args TARGET=macos-arm64 CONFIG=debug GN_ARGS='rtc_use_h264=false')" +printf '%s\n' "$debug" | grep -q 'is_debug = true' +printf '%s\n' "$debug" | grep -q 'target_os = "mac"' +printf '%s\n' "$debug" | grep -q 'rtc_use_h264 = false' + +android="$("${layout_make[@]}" -s print-gn-args TARGET=android-arm64-v8a)" +printf '%s\n' "$android" | grep -q 'target_os = "android"' +printf '%s\n' "$android" | grep -q 'target_cpu = "arm64"' + +ninja_target="$("$ROOT/scripts/gn-gen.sh" --ninja-target ios-arm64-device)" +[[ "$ninja_target" == framework_objc ]] + +banner="$(make --no-print-directory announce VERB=build PLATFORM=ios CONFIG=release)" +printf '%s\n' "$banner" | grep -q '==> build ios' +printf '%s\n' "$banner" | grep -q 'config: release' +printf '%s\n' "$banner" | grep -q 'deps_root:' +printf '%s\n' "$banner" | grep -q '/ios$' +printf '%s\n' "$banner" | grep 'slices:' | grep -q 'catalyst-arm64' +printf '%s\n' "$banner" | grep 'slices:' | grep -q 'catalyst-x64' + +ios_skip="$(make --no-print-directory announce VERB=package PLATFORM=ios SKIP_MACCATALYST=1)" +printf '%s\n' "$ios_skip" | grep -q 'skip_maccatalyst: 1' +printf '%s\n' "$ios_skip" | grep 'slices:' | grep -q 'ios-arm64-device' +! printf '%s\n' "$ios_skip" | grep 'slices:' | grep -q 'catalyst' + +macos_banner="$(make --no-print-directory announce VERB=package PLATFORM=macos SKIP_MACCATALYST=1)" +printf '%s\n' "$macos_banner" | grep -q '/macos$' +printf '%s\n' "$macos_banner" | grep 'slices:' | grep -q 'macos-arm64' +printf '%s\n' "$macos_banner" | grep 'slices:' | grep -q 'macos-x64' + +test_banner="$(make --no-print-directory announce VERB=test PLATFORM=macos CONFIG=release)" +printf '%s\n' "$test_banner" | grep -q '==> test macos' +printf '%s\n' "$test_banner" | grep -q 'config: debug (tests always debug)' + +empty="$(mktemp -d)" +combine_none="$( + make combine PRODUCTS="$empty" SKIP_LICENSES=1 \ + DEPS_ROOT="$layout" WEBRTC_SRC="$layout/src" 2>&1 || true +)" +printf '%s\n' "$combine_none" | grep -q 'no WebRTC.xcframework' +rm -rf "$empty" + +one="$(mktemp -d)" +mkdir -p "$one/ios/WebRTC.xcframework" +printf 'stub\n' > "$one/ios/WebRTC.xcframework/Info.plist" +make combine PRODUCTS="$one" SKIP_LICENSES=1 \ + DEPS_ROOT="$layout" WEBRTC_SRC="$layout/src" +[[ -f "$one/WebRTC.xcframework/Info.plist" ]] +rm -rf "$one" + +rename_root="$(mktemp -d)" +rename_src="$rename_root/WebRTC.xcframework" +mkdir -p "$rename_src/ios-arm64/WebRTC.framework/Headers" +mkdir -p "$rename_src/ios-arm64/WebRTC.framework/Modules" +printf '%s\n' 'CFBundleNameWebRTC' \ + > "$rename_src/Info.plist" +printf '%s\n' 'framework module WebRTC { umbrella header "WebRTC.h" }' \ + > "$rename_src/ios-arm64/WebRTC.framework/Modules/module.modulemap" +printf '%s\n' '#import ' \ + > "$rename_src/ios-arm64/WebRTC.framework/Headers/WebRTC.h" +printf 'stub\n' > "$rename_src/ios-arm64/WebRTC.framework/WebRTC" +printf '%s\n' '' \ + > "$rename_src/ios-arm64/WebRTC.framework/Info.plist" +rename_out="$(mktemp -d)" +make rename apple XCFRAMEWORK="$rename_src" RENAMED="$rename_out" \ + DEPS_ROOT="$layout" WEBRTC_SRC="$layout/src" +[[ -d "$rename_src/ios-arm64/WebRTC.framework" ]] +[[ -d "$rename_out/StreamWebRTC.xcframework/ios-arm64/StreamWebRTC.framework" ]] +grep -q 'StreamWebRTC' "$rename_out/StreamWebRTC.xcframework/ios-arm64/StreamWebRTC.framework/Modules/module.modulemap" +grep -q 'import "$aar_dir/libwebrtc.aar" +make rename android AAR="$aar_dir/libwebrtc.aar" RENAMED="$aar_dir/renamed" \ + DEPS_ROOT="$layout" WEBRTC_SRC="$layout/src" +[[ -f "$aar_dir/libwebrtc.aar" ]] +[[ -f "$aar_dir/renamed/libwebrtc.aar" ]] +cmp -s "$aar_dir/libwebrtc.aar" "$aar_dir/renamed/libwebrtc.aar" +rm -rf "$aar_dir" "$(dirname "$layout")" + +wrong="$(mktemp -d)/not-src" +init_git_tree "$wrong" +wrong_root="$(dirname "$wrong")" +if WEBRTC_SRC="$wrong" DEPS_ROOT="$wrong_root" \ + "$ROOT/scripts/bootstrap.sh" --check 2>"$wrong.err"; then + echo "expected --check to fail on non-src checkout" >&2 + exit 1 +fi +grep -q 'run: make bootstrap' "$wrong.err" +[[ "$(cat "$wrong.err")" == "run: make bootstrap" ]] + +# help/check/bootstrap stay ungated; every other user verb hits the guard. +help_wrong="$(WEBRTC_SRC="$wrong" DEPS_ROOT="$wrong_root" make -s help)" +printf '%s\n' "$help_wrong" | grep -q 'make bootstrap' + +expect_make_bootstrap() { + local err="$wrong.err" + if WEBRTC_SRC="$wrong" DEPS_ROOT="$wrong_root" \ + make --no-print-directory "$@" 2>"$err"; then + echo "expected make $* to fail on non-src checkout" >&2 + exit 1 + fi + grep -q 'run: make bootstrap' "$err" +} + +expect_make_bootstrap build ios +expect_make_bootstrap test macos +expect_make_bootstrap package ios +expect_make_bootstrap deps +expect_make_bootstrap runhooks +expect_make_bootstrap combine +expect_make_bootstrap rename apple +expect_make_bootstrap clean +expect_make_bootstrap print-gn-args TARGET=ios-arm64-device +rm -rf "$wrong_root" "$wrong.err" + +# 1-3 pass without .gclient; deps/build/test/package still need it. +bare="$(mktemp -d)/webrtc" +mkdir -p "$bare/src" +printf 'hooks = []\n' > "$bare/src/DEPS" +WEBRTC_SRC="$bare/src" DEPS_ROOT="$bare" \ + "$ROOT/scripts/bootstrap.sh" --check +if WEBRTC_SRC="$bare/src" DEPS_ROOT="$bare" \ + "$ROOT/scripts/bootstrap.sh" --check --gclient 2>"$bare.err"; then + echo "expected --check --gclient to fail without .gclient" >&2 + exit 1 +fi +grep -q 'run: make bootstrap' "$bare.err" +if WEBRTC_SRC="$bare/src" DEPS_ROOT="$bare" \ + make --no-print-directory build ios 2>"$bare.err"; then + echo "expected make build to fail without .gclient" >&2 + exit 1 +fi +grep -q 'run: make bootstrap' "$bare.err" +bare_rename="$( + WEBRTC_SRC="$bare/src" DEPS_ROOT="$bare" \ + make --no-print-directory rename 2>&1 || true +)" +printf '%s\n' "$bare_rename" | grep -q 'usage: make rename' +! printf '%s\n' "$bare_rename" | grep -q 'run: make bootstrap' +rm -rf "$(dirname "$bare")" "$bare.err" + +# Named webrtc (git) -> webrtc/src. Parent .gclient + wrapper Makefile. +parent="$(mktemp -d)" +repo="$parent/webrtc" +init_git_tree "$repo" +CONFIRM=1 BOOTSTRAP_SRC="$repo" "$ROOT/scripts/bootstrap.sh" >/dev/null +[[ -f "$parent/webrtc/src/DEPS" ]] +[[ -f "$parent/webrtc/.gclient" ]] +[[ -f "$parent/webrtc/Makefile" ]] +grep -q 'created by bootstrap if missing' "$parent/webrtc/Makefile" +grep -q 'src/stream_build' "$parent/webrtc/Makefile" + +mkdir -p "$parent/webrtc/src/stream_build" +cat > "$parent/webrtc/src/stream_build/Makefile" <<'STUB' +.DEFAULT_GOAL := help +FIRST := $(firstword $(MAKECMDGOALS)) +REST := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) +.PHONY: help $(MAKECMDGOALS) +help: + @echo "stub-help CONFIG=$(CONFIG) JOBS=$(JOBS)" +ifneq ($(FIRST),) +ifneq ($(FIRST),help) +$(FIRST): + @echo "stub-goals $(MAKECMDGOALS) CONFIG=$(CONFIG) JOBS=$(JOBS)" +endif +endif +ifneq ($(REST),) +$(REST): + @: +endif +STUB +fwd="$(make -C "$parent/webrtc" --no-print-directory build ios CONFIG=debug JOBS=8)" +printf '%s\n' "$fwd" | grep -q 'stub-goals build ios' +printf '%s\n' "$fwd" | grep -q 'CONFIG=debug' +printf '%s\n' "$fwd" | grep -q 'JOBS=8' +[[ "$(printf '%s\n' "$fwd" | grep -c 'stub-goals')" == 1 ]] +fwd_all="$(make -C "$parent/webrtc" --no-print-directory)" +printf '%s\n' "$fwd_all" | grep -q 'stub-help' + +printf '%s\n' 'all:' $'\t@echo foreign' > "$parent/webrtc/Makefile" +grep -q '"managed": False' "$parent/webrtc/.gclient" +! grep -q '"revision"' "$parent/webrtc/.gclient" +[[ ! -L "$parent/webrtc/src" ]] +[[ -d "$parent/webrtc/src/.git" ]] +git -C "$parent/webrtc/src" rev-parse --is-inside-work-tree >/dev/null +CONFIRM=1 BOOTSTRAP_SRC="$parent/webrtc/src" "$ROOT/scripts/bootstrap.sh" >/dev/null +[[ -f "$parent/webrtc/.gclient" ]] +grep -q foreign "$parent/webrtc/Makefile" +rm -f "$parent/webrtc/Makefile" +CONFIRM=1 BOOTSTRAP_SRC="$parent/webrtc/src" "$ROOT/scripts/bootstrap.sh" >/dev/null +grep -q 'created by bootstrap if missing' "$parent/webrtc/Makefile" +WEBRTC_SRC="$parent/webrtc/src" DEPS_ROOT="$parent/webrtc" \ + "$ROOT/scripts/bootstrap.sh" --check +WEBRTC_SRC="$parent/webrtc/src" DEPS_ROOT="$parent/webrtc" \ + "$ROOT/scripts/bootstrap.sh" --check --gclient + +symlink_parent="$(mktemp -d)/webrtc" +mkdir -p "$symlink_parent" +real_src="$(mktemp -d)/real" +init_git_tree "$real_src" +ln -sfn "$real_src" "$symlink_parent/src" +if WEBRTC_SRC="$symlink_parent/src" DEPS_ROOT="$symlink_parent" \ + "$ROOT/scripts/bootstrap.sh" --check 2>"$symlink_parent.err"; then + echo "expected --check to fail on symlink src" >&2 + exit 1 +fi +grep -q 'run: make bootstrap' "$symlink_parent.err" +if CONFIRM=1 BOOTSTRAP_SRC="$symlink_parent/src" \ + "$ROOT/scripts/bootstrap.sh" 2>"$symlink_parent.err"; then + echo "expected bootstrap to refuse symlink src" >&2 + exit 1 +fi +grep -q 'refuses symlink src' "$symlink_parent.err" +rm -rf "$parent" "$(dirname "$symlink_parent")" "$(dirname "$real_src")" \ + "$symlink_parent.err" + +# Non-interactive without CONFIRM=1 prints the plan and exits. +ni_parent="$(mktemp -d)" +ni_repo="$ni_parent/webrtc" +init_git_tree "$ni_repo" +ni_out="$(BOOTSTRAP_SRC="$ni_repo" "$ROOT/scripts/bootstrap.sh" 2>&1 || true)" +printf '%s\n' "$ni_out" | grep -q 'CONFIRM=1' +printf '%s\n' "$ni_out" | grep -q "mv $ni_repo" +[[ -d "$ni_repo/.git" ]] +rm -rf "$ni_parent" + +deps_tmp="$(mktemp -d)" +fake_bin="$deps_tmp/bin" +mkdir -p "$fake_bin" "$deps_tmp/webrtc/src" "$deps_tmp/webrtc/.gclient-git-cache" +init_git_tree "$deps_tmp/webrtc/src" +cat > "$fake_bin/gclient" <<'FAKE' +#!/usr/bin/env bash +printf '%s\n' "$*" >> "${FAKE_GCLIENT_LOG}" +if [[ "${1:-}" == sync ]]; then + printf '%s\n' "$*" > "${FAKE_GCLIENT_SYNC_ARGS}" + printf '%s\n' "${GIT_CACHE_PATH-}" > "${FAKE_GCLIENT_CACHE}" +fi +exit 0 +FAKE +chmod +x "$fake_bin/gclient" +export FAKE_GCLIENT_LOG="$deps_tmp/log" +export FAKE_GCLIENT_SYNC_ARGS="$deps_tmp/sync_args" +export FAKE_GCLIENT_CACHE="$deps_tmp/cache_env" +wt_before="$(git -C "$deps_tmp/webrtc/src" worktree list | wc -l | tr -d ' ')" +deps_out="$( + PATH="$fake_bin:$PATH" \ + DEPS_ROOT="$deps_tmp/webrtc" \ + WEBRTC_SRC="$deps_tmp/webrtc/src" \ + GIT_CACHE_PATH="$deps_tmp/webrtc/.gclient-git-cache" \ + RUN_HOOKS=0 JOBS=2 \ + "$ROOT/scripts/deps.sh" sync +)" +printf '%s\n' "$deps_out" | grep -q 'will not reset it' +printf '%s\n' "$deps_out" | grep -q 'running: gclient sync -j2 --no-history --shallow --nohooks' +! printf '%s\n' "$deps_out" | grep -q -- '--revision' +grep -q '"managed": False' "$deps_tmp/webrtc/.gclient" +! grep -q '"revision"' "$deps_tmp/webrtc/.gclient" +grep -q 'sync -j2 --no-history --shallow --nohooks' "$deps_tmp/sync_args" +! grep -q -- '--revision' "$deps_tmp/sync_args" +grep -q "$deps_tmp/webrtc/.gclient-git-cache" "$deps_tmp/cache_env" +mkdir -p "$deps_tmp/webrtc/src/third_party/.git/objects/info" +printf '%s\n' \ + "$deps_tmp/webrtc/.gclient_deps/.gclient-git-cache/fake-repo/objects" \ + > "$deps_tmp/webrtc/src/third_party/.git/objects/info/alternates" +PATH="$fake_bin:$PATH" \ + DEPS_ROOT="$deps_tmp/webrtc" \ + WEBRTC_SRC="$deps_tmp/webrtc/src" \ + GIT_CACHE_PATH="$deps_tmp/webrtc/.gclient-git-cache" \ + RUN_HOOKS=0 JOBS=2 \ + "$ROOT/scripts/deps.sh" sync >/dev/null +grep -qx "$deps_tmp/webrtc/.gclient-git-cache/fake-repo/objects" \ + "$deps_tmp/webrtc/src/third_party/.git/objects/info/alternates" +! grep -q '.gclient_deps/.gclient-git-cache' \ + "$deps_tmp/webrtc/src/third_party/.git/objects/info/alternates" +printf '%s\n' \ + "/home/runner/work/webrtc/webrtc/.gclient-git-cache/fake-repo/objects" \ + > "$deps_tmp/webrtc/src/third_party/.git/objects/info/alternates" +PATH="$fake_bin:$PATH" \ + DEPS_ROOT="$deps_tmp/webrtc" \ + WEBRTC_SRC="$deps_tmp/webrtc/src" \ + GIT_CACHE_PATH="$deps_tmp/webrtc/.gclient-git-cache" \ + RUN_HOOKS=0 JOBS=2 \ + "$ROOT/scripts/deps.sh" sync >/dev/null +grep -qx "$deps_tmp/webrtc/.gclient-git-cache/fake-repo/objects" \ + "$deps_tmp/webrtc/src/third_party/.git/objects/info/alternates" +! grep -q '/home/runner/work' \ + "$deps_tmp/webrtc/src/third_party/.git/objects/info/alternates" +PATH="$fake_bin:$PATH" \ + DEPS_ROOT="$deps_tmp/webrtc" \ + WEBRTC_SRC="$deps_tmp/webrtc/src" \ + GIT_CACHE_PATH="$deps_tmp/webrtc/.gclient-git-cache" \ + RUN_HOOKS=0 JOBS=2 SHALLOW=0 \ + "$ROOT/scripts/deps.sh" sync >/dev/null +grep -q 'sync -j2 --nohooks' "$deps_tmp/sync_args" +! grep -q -- '--no-history' "$deps_tmp/sync_args" +! grep -q -- '--shallow' "$deps_tmp/sync_args" +[[ ! -L "$deps_tmp/webrtc/src" ]] +[[ -f "$deps_tmp/webrtc/src/DEPS" ]] +wt_after="$(git -C "$deps_tmp/webrtc/src" worktree list | wc -l | tr -d ' ')" +[[ "$wt_before" == "$wt_after" ]] +python3 - "$deps_tmp/webrtc" <<'PY' +import os +import sys +prefix = os.path.realpath(sys.argv[1]) +gn = os.path.abspath(os.path.join(prefix, "src/build/config/gclient_args.gni")) +real_gn = os.path.realpath(gn) +if os.path.commonpath([prefix, real_gn]) != prefix: + raise SystemExit("gclient_gn_args_file would escape %r -> %r" % (prefix, real_gn)) +PY +if PATH="$fake_bin:$PATH" \ + DEPS_ROOT="$deps_tmp/webrtc" \ + WEBRTC_SRC="$deps_tmp/webrtc/src" \ + WEBRTC_REVISION=eeff9252f32a40d1671974c31c096ce9fa776130 \ + "$ROOT/scripts/deps.sh" sync 2>"$deps_tmp/pin_err"; then + echo "expected pin to fail on seeded src" >&2 + exit 1 +fi +grep -q 'src is seeded from your git checkout; will not reset it' "$deps_tmp/pin_err" + +missing="$(mktemp -d)/webrtc" +mkdir -p "$missing" +if PATH="$fake_bin:$PATH" DEPS_ROOT="$missing" \ + "$ROOT/scripts/deps.sh" sync 2>"$deps_tmp/missing_err"; then + echo "expected deps.sh to fail without src" >&2 + exit 1 +fi +grep -q 'run: make bootstrap' "$deps_tmp/missing_err" + +rm -rf "$deps_tmp/linkroot" +mkdir -p "$deps_tmp/linkroot" +ln -sfn "$deps_tmp/webrtc/src" "$deps_tmp/linkroot/src" +if PATH="$fake_bin:$PATH" DEPS_ROOT="$deps_tmp/linkroot" \ + "$ROOT/scripts/deps.sh" sync 2>"$deps_tmp/link_err"; then + echo "expected deps.sh to refuse symlink src" >&2 + exit 1 +fi +grep -q 'src is a symlink' "$deps_tmp/link_err" +rm -rf "$deps_tmp" "$missing" + +gha="$ROOT/../.github" +[[ ! -e "$gha/actions/artifact-upload/action.yml" ]] +! grep -q 'github.run_id' "$gha/actions/artifact-download/action.yml" +! grep -q 'github.run_id' "$gha/actions/artifact-put/action.yml" +! grep -q 'WEBRTC_REF' "$gha/actions/artifact-download/action.yml" +! grep -q 'WEBRTC_REF' "$gha/actions/artifact-put/action.yml" +! grep -q 'webrtc_ref:' "$gha/actions/artifact-download/action.yml" +! grep -q 'webrtc_ref:' "$gha/actions/artifact-put/action.yml" +! grep -q 'secrets\.' "$gha/actions/artifact-download/action.yml" +! grep -q 'secrets\.' "$gha/actions/artifact-put/action.yml" +! grep -q 'secrets\.' "$gha/actions/restore-tree/action.yml" +grep -q 'tar cf "${tar_file}"' "$gha/actions/artifact-put/action.yml" +! grep -q 'tar cf -' "$gha/actions/artifact-put/action.yml" +! grep -q 's3 cp -' "$gha/actions/artifact-put/action.yml" +! grep -q 's3 cp "s3://${BUCKET}/${object}" -' \ + "$gha/actions/artifact-download/action.yml" +! grep -q 's3 cp "s3://${BUCKET}/${object}" "${tar_file}"' \ + "$gha/actions/artifact-download/action.yml" +grep -q 's3api get-object' "$gha/actions/artifact-download/action.yml" +grep -q 'bytes=${have}-' "$gha/actions/artifact-download/action.yml" +grep -q 'resume_from=' "$gha/actions/artifact-download/action.yml" +grep -q 's3 cp "${tar_file}" "s3://${BUCKET}/${object}"' \ + "$gha/actions/artifact-put/action.yml" +grep -q 'AWS_MAX_ATTEMPTS' "$gha/actions/artifact-download/action.yml" +grep -q 'AWS_RETRY_MODE' "$gha/actions/artifact-download/action.yml" +grep -q 'cli-read-timeout' "$gha/actions/artifact-download/action.yml" +grep -q 'size mismatch' "$gha/actions/artifact-download/action.yml" +grep -q 'tar xf "${tar_file}"' "$gha/actions/artifact-download/action.yml" +grep -q '.hetzner-hit-bytes' "$gha/actions/artifact-download/action.yml" +grep -q '.hetzner-hit-bytes' "$gha/actions/artifact-put/action.yml" +grep -q '1073741824' "$gha/actions/artifact-put/action.yml" +grep -q 'skip upload:' "$gha/actions/artifact-put/action.yml" +grep -q 'object="artifacts/\${{ github.repository }}/\${OBJECT_STEM}.tar"' \ + "$gha/actions/artifact-put/action.yml" +grep -q 'object="artifacts/\${{ github.repository }}/\${OBJECT_STEM}.tar"' \ + "$gha/actions/artifact-download/action.yml" +! grep -qE 'name: Deps$' "$gha/workflows/_make.yml" +! grep -q 'name: Hetzner backfill' "$gha/workflows/_make.yml" +! grep -q 'path: deps-key' "$gha/workflows/_make.yml" +! grep -q 'name: deps-key' "$gha/workflows/_make.yml" +grep -q 'cache_key: build-ios' "$gha/workflows/_make.yml" +grep -q 'cache_key: build-macos' "$gha/workflows/_make.yml" +grep -q 'cache_key: build-android' "$gha/workflows/_make.yml" +grep -q 'include-hidden-files: true' "$gha/workflows/_make.yml" +grep -q 'compression-level: 0' "$gha/workflows/_make.yml" +grep -A5 'name: Build iOS' "$gha/workflows/_make.yml" | grep -q 'needs: \[plan\]' +! grep -A8 'name: Build iOS' "$gha/workflows/_make.yml" | grep -q deps +grep -A5 'name: Test iOS' "$gha/workflows/_make.yml" | grep -q 'needs: \[plan\]' +! grep -A8 'name: Test iOS' "$gha/workflows/_make.yml" | grep -q deps +grep -q 'artifact-download' "$gha/actions/restore-tree/action.yml" +grep -q 'hetzner_access_key' "$gha/actions/restore-tree/action.yml" +grep -q 'actions/download-artifact' "$gha/actions/restore-tree/action.yml" +! grep -q 'tar xf' "$gha/actions/restore-tree/action.yml" +grep -q 'if_missing: skip' "$gha/actions/restore-tree/action.yml" +grep -q 'SHALLOW: "1"' "$gha/actions/restore-tree/action.yml" +grep -q 'RUN_HOOKS: "1"' "$gha/actions/restore-tree/action.yml" +! grep -q 'RUN_HOOKS: "0"' "$gha/workflows/_make.yml" +! grep -q 'Drop git-cache alternates' "$gha/actions/restore-tree/action.yml" +grep -q 'GIT_CACHE_PATH: ${{ github.workspace }}/.gclient-git-cache' \ + "$gha/actions/restore-tree/action.yml" +grep -q 'rewrite_git_cache_alternates' "$ROOT/scripts/deps.sh" +grep -q '.gclient-git-cache' "$gha/actions/artifact-put/action.yml" +grep -q 'src/resources' "$gha/actions/artifact-put/action.yml" +grep -q '.cipd' "$gha/actions/artifact-put/action.yml" +! grep -q 'src/third_party' "$gha/actions/artifact-put/action.yml" +! grep -q 'src/third_party' "$gha/actions/artifact-download/action.yml" +! grep -q 'src/third_party' "$gha/actions/restore-tree/action.yml" +grep -A12 'members=(.gclient-git-cache)' "$gha/actions/artifact-put/action.yml" | \ + grep -q 'src/resources' +grep -A12 'members=(.gclient-git-cache)' "$gha/actions/artifact-put/action.yml" | \ + grep -q '.cipd' +! grep -A12 'members=(.gclient-git-cache)' "$gha/actions/artifact-put/action.yml" | \ + grep -q 'third_party' +grep -q 'pack members:' "$gha/actions/artifact-put/action.yml" +grep -q 'uses: ./src/.github/actions/artifact-put' "$gha/workflows/_make.yml" + +echo "ok" diff --git a/stream_build/scripts/combine-apple.sh b/stream_build/scripts/combine-apple.sh new file mode 100755 index 0000000000..491ae2dcba --- /dev/null +++ b/stream_build/scripts/combine-apple.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# Discover platform xcframeworks under PRODUCTS/*/ and emit one WebRTC.xcframework. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +require_darwin +require_cmd xcodebuild + +SRC="${WEBRTC_SRC:-}" +OUT="" +PRODUCTS="" +NAME="WebRTC" +ZIP=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --src) SRC="$2"; shift 2 ;; + --out) OUT="$2"; shift 2 ;; + --products) PRODUCTS="$2"; shift 2 ;; + --name) NAME="$2"; shift 2 ;; + --zip) ZIP=1; shift ;; + *) die "combine-apple.sh: unknown flag $1" ;; + esac +done + +[[ -n "$PRODUCTS" ]] || die "combine-apple.sh requires --products" + +shopt -s nullglob +found=() +for xcf in "$PRODUCTS"/*/"${NAME}.xcframework"; do + [[ -d "$xcf" ]] && found+=("$xcf") +done +shopt -u nullglob + +if [[ ${#found[@]} -eq 0 ]]; then + die "no ${NAME}.xcframework under ${PRODUCTS}/*/ — run make package ios and/or make package macos" +fi + +echo "combine: found ${#found[@]} platform xcframework(s):" +for xcf in "${found[@]}"; do + echo " $xcf" +done + +dest="${PRODUCTS}/${NAME}.xcframework" +rm -rf "$dest" + +if [[ ${#found[@]} -eq 1 ]]; then + echo "combine: one platform — copying ${found[0]} -> $dest" + cp -R "${found[0]}" "$dest" +else + require_cmd find + xc_args=(-create-xcframework) + added=0 + while IFS= read -r fw; do + [[ -d "$fw" ]] || continue + xc_args+=(-framework "$fw") + dsym="" + if [[ -d "${fw}.dSYM" ]]; then + dsym="${fw}.dSYM" + elif [[ -d "$(dirname "$fw")/dSYMs/$(basename "$fw").dSYM" ]]; then + dsym="$(dirname "$fw")/dSYMs/$(basename "$fw").dSYM" + fi + if [[ -n "$dsym" ]]; then + xc_args+=(-debug-symbols "$dsym") + fi + added=1 + done < <(find "${found[@]}" -name '*.framework' -type d | sort) + [[ "$added" -eq 1 ]] || die "no .framework slices inside: ${found[*]}" + xc_args+=(-output "$dest") + echo "xcodebuild ${xc_args[*]}" + xcodebuild "${xc_args[@]}" +fi + +if [[ "${SKIP_LICENSES:-0}" == 1 ]]; then + echo "skipping license generation (SKIP_LICENSES=1)" +else + [[ -n "$SRC" ]] || die "combine-apple.sh requires --src (or WEBRTC_SRC) to generate licenses" + [[ -n "$OUT" ]] || die "combine-apple.sh requires --out to generate licenses" + require_cmd python3 + license_script="$SRC/tools_webrtc/libs/generate_licenses.py" + [[ -f "$license_script" ]] || die "missing $license_script" + + gn_targets=() + build_dirs=() + seen_ios=0 + seen_macos=0 + for xcf in "${found[@]}"; do + platform="$(basename "$(dirname "$xcf")")" + case "$platform" in + macos) + if [[ "$seen_macos" -eq 0 ]]; then + gn_targets+=(--target "//sdk:mac_framework_objc") + seen_macos=1 + fi + for dir in "$OUT"/macos-*; do + [[ -d "$dir" ]] && build_dirs+=("$dir") + done + ;; + *) + if [[ "$seen_ios" -eq 0 ]]; then + gn_targets+=(--target "//sdk:framework_objc") + seen_ios=1 + fi + if [[ "$platform" == ios ]]; then + for dir in "$OUT"/ios-* "$OUT"/catalyst-*; do + [[ -d "$dir" ]] && build_dirs+=("$dir") + done + else + for dir in "$OUT/${platform}-"*; do + [[ -d "$dir" ]] && build_dirs+=("$dir") + done + fi + ;; + esac + done + [[ ${#gn_targets[@]} -gt 0 ]] || die "no license GN targets for: ${found[*]}" + [[ ${#build_dirs[@]} -gt 0 ]] || die "no slice out dirs under $OUT for license generation" + echo "python3 $license_script ${gn_targets[*]} $dest ${build_dirs[*]}" + python3 "$license_script" "${gn_targets[@]}" "$dest" "${build_dirs[@]}" + echo "wrote ${dest}/LICENSE.md" +fi + +if [[ "$ZIP" -eq 1 ]]; then + if command -v ditto >/dev/null 2>&1; then + ditto -c -k --sequesterRsrc --keepParent \ + "$dest" \ + "${PRODUCTS}/${NAME}.xcframework.zip" + else + ( + cd "$PRODUCTS" + zip --symlinks -r "${NAME}.xcframework.zip" "${NAME}.xcframework" + ) + fi +fi + +echo "wrote $dest" diff --git a/stream_build/scripts/common.sh b/stream_build/scripts/common.sh new file mode 100755 index 0000000000..4e3ffaebff --- /dev/null +++ b/stream_build/scripts/common.sh @@ -0,0 +1,335 @@ +#!/usr/bin/env bash +# Shared helpers for the WebRTC Makefile wrapper. +set -euo pipefail + +PIPELINE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +GN_DIR="${PIPELINE_DIR}/gn" + +die() { + echo "error: $*" >&2 + exit 1 +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "required tool '$1' not found in PATH" +} + +host_uname() { + uname -s +} + +require_darwin() { + [[ "$(host_uname)" == Darwin ]] || die "Apple targets require macOS" +} + +require_linux() { + [[ "$(host_uname)" == Linux ]] || die "Android AAR builds require Linux" +} + +require_windows() { + case "$(host_uname)" in + MINGW*|MSYS*|CYGWIN*) return 0 ;; + esac + [[ "${OS:-}" == Windows_NT ]] || die "Windows targets require Windows" +} + +require_webrtc_src() { + local src="${1:-}" + [[ -n "$src" ]] || die "WEBRTC_SRC or WEBRTC_ROOT is required" + [[ -f "$src/DEPS" ]] || die "No WebRTC checkout at $src (missing DEPS)" +} + +quote_target_os() { + local raw="$1" + local os first=1 + printf '[' + # shellcheck disable=SC2086 + for os in ${raw//,/ }; do + [[ -z "$os" ]] && continue + if [[ $first -eq 1 ]]; then + first=0 + else + printf ', ' + fi + printf '"%s"' "$os" + done + printf ']\n' +} + +# Write Chromium .gclient at the gclient parent. managed: False, no revision. +write_gclient() { + local dest="$1" + local repo="${2:-${WEBRTC_REPO:-git@github.com:GetStream/webrtc.git}}" + local target_os="${3:-${TARGET_OS:-ios}}" + cat >"${dest}/.gclient" < str: + idx = line.find(frag) + if idx < 0: + return line + start = idx + while start > 0 and line[start - 1] not in stops: + start -= 1 + old_path = line[start : idx + len(frag)].rstrip("/") + if old_path == new_cache: + return line + return line[:start] + new_cache + line[idx + len(frag) :] + + +repos = set() +for path in listing.splitlines(): + if not path: + continue + try: + with open(path, encoding="utf-8", errors="replace") as fh: + text = fh.read() + except OSError: + continue + if frag not in text: + continue + rewritten = "".join(rewrite_line(line) for line in text.splitlines(True)) + if rewritten == text: + continue + with open(path, "w", encoding="utf-8") as fh: + fh.write(rewritten) + marker = "/.git/" + i = path.find(marker) + repos.add(path[:i] if i >= 0 else path) + +if repos: + print("rewrote git-cache paths in %d repos" % len(repos)) +PY +} + +abspath() { + local path="$1" + (cd "$(dirname "$path")" && printf '%s/%s\n' "$(pwd)" "$(basename "$path")") +} + +resolve_gn() { + local src="${1:-}" + local os + os="$(host_uname)" + local candidate="" + case "$os" in + Darwin) candidate="$src/buildtools/mac/gn" ;; + Linux) candidate="$src/buildtools/linux64/gn" ;; + MINGW*|MSYS*|CYGWIN*) candidate="$src/buildtools/win/gn.exe" ;; + esac + if [[ -n "$candidate" && -x "$candidate" ]]; then + printf '%s\n' "$candidate" + return + fi + command -v gn +} + +resolve_ninja() { + local src="${1:-}" + local bundled="$src/third_party/ninja/ninja" + if [[ -x "$bundled" ]]; then + printf '%s\n' "$bundled" + return + fi + command -v ninja +} + +is_apple_slice() { + local name="$1" + [[ "$name" == ios-* || "$name" == catalyst-* || "$name" == macos-* ]] +} + +is_android_slice() { + local name="$1" + [[ "$name" == android-* ]] +} + +is_windows_slice() { + local name="$1" + [[ "$name" == windows-* ]] +} + +slice_line() { + local name="$1" + local line + line="$(awk -F'\t' -v n="$name" '$1 == n { print; exit }' "${GN_DIR}/slices.tsv")" + [[ -n "$line" ]] || die "unknown slice '$name' (see gn/slices.tsv)" + printf '%s\n' "$line" +} + +slice_ninja_target() { + local name="$1" + slice_line "$name" | awk -F'\t' '{ print $2 }' +} + +slice_gn_args() { + local name="$1" + slice_line "$name" | awk -F'\t' '{ print $3 }' +} + +# Convert "key=value" / "key = value" tokens into args.gn lines. +gn_tokens_to_lines() { + local token key value + for token in "$@"; do + [[ -z "$token" ]] && continue + key="${token%%=*}" + value="${token#*=}" + key="${key%"${key##*[![:space:]]}"}" + key="${key#"${key%%[![:space:]]*}"}" + value="${value#"${value%%[![:space:]]*}"}" + printf '%s = %s\n' "$key" "$value" + done +} + +cat_gn_file() { + local path="$1" + [[ -f "$path" ]] || die "GN args file not found: $path" + grep -v '^[[:space:]]*#' "$path" | grep -v '^[[:space:]]*$' || true +} + +# Compose args.gn content. Reads env/flags via positional: +# compose_gn_args --config release --slice NAME --overlay FILE --extra TOKENS +compose_gn_args() { + local config="release" + local slice="" + local extra="" + local overlays=() + + while [[ $# -gt 0 ]]; do + case "$1" in + --config) config="$2"; shift 2 ;; + --slice) slice="$2"; shift 2 ;; + --overlay) + overlays+=("$2") + shift 2 + ;; + --extra) extra="${2:-}"; shift 2 ;; + *) die "compose_gn_args: unknown flag $1" ;; + esac + done + + cat_gn_file "${GN_DIR}/common.args" + if [[ -n "$slice" ]] && is_apple_slice "$slice"; then + cat_gn_file "${GN_DIR}/apple.args" + fi + if [[ -n "$slice" ]] && is_android_slice "$slice"; then + cat_gn_file "${GN_DIR}/android.args" + fi + if [[ -n "$slice" ]] && is_windows_slice "$slice"; then + cat_gn_file "${GN_DIR}/windows.args" + fi + local overlay + for overlay in "${overlays[@]+"${overlays[@]}"}"; do + [[ -z "$overlay" ]] && continue + if [[ -f "$overlay" ]]; then + cat_gn_file "$overlay" + elif [[ -f "${GN_DIR}/${overlay}.args" ]]; then + cat_gn_file "${GN_DIR}/${overlay}.args" + else + die "unknown GN overlay '$overlay'" + fi + done + if [[ "$config" == debug ]]; then + echo 'is_debug = true' + else + echo 'is_debug = false' + fi + if [[ -n "$slice" ]]; then + # shellcheck disable=SC2086 + gn_tokens_to_lines $(slice_gn_args "$slice") + fi + if [[ -n "$extra" ]]; then + # shellcheck disable=SC2086 + gn_tokens_to_lines $extra + fi +} + +flatten_gn_args() { + awk ' + /^[[:space:]]*$/ { next } + /^[[:space:]]*#/ { next } + { + line = $0 + sub(/^[[:space:]]+/, "", line) + sub(/[[:space:]]+$/, "", line) + split(line, parts, " = ") + if (length(parts) >= 2) { + value = substr(line, index(line, " = ") + 3) + printf "%s=%s\n", parts[1], value + } + } + ' +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + cmd="${1:-}" + shift || true + case "$cmd" in + gn) resolve_gn "${1:-}" ;; + ninja) resolve_ninja "${1:-}" ;; + slice-ninja) slice_ninja_target "${1:-}" ;; + *) die "usage: common.sh gn|ninja|slice-ninja ..." ;; + esac +fi diff --git a/stream_build/scripts/deps.sh b/stream_build/scripts/deps.sh new file mode 100755 index 0000000000..ce84d948e4 --- /dev/null +++ b/stream_build/scripts/deps.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# gclient config + sync for GetStream/webrtc. +# DEPS_ROOT is the gclient parent (named webrtc). src there is this git +# checkout, not a second clone/worktree and not a symlink to the git root. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +usage() { + cat <<'EOF' +usage: deps.sh sync|runhooks + +env: + DEPS_ROOT gclient parent (required). Default from make: parent of src + WEBRTC_SRC this git checkout (DEPS_ROOT/src). Not cloned again. + TARGET_OS comma/space list, default: ios + JOBS gclient -j, default: 8 + SHALLOW 1 (default) adds --no-history --shallow; 0 for full history + RUN_HOOKS 1 to run hooks during sync, 0 for --nohooks + WEBRTC_REPO default: git@github.com:GetStream/webrtc.git + GIT_CACHE_PATH default: DEPS_ROOT/.gclient-git-cache + WEBRTC_REVISION refused (src is this worktree; gclient must not reset it) + WEBRTC_REF refused (same as WEBRTC_REVISION) +EOF +} + +DEPS_ROOT="${DEPS_ROOT:-${WEBRTC_ROOT:-}}" +WEBRTC_SRC="${WEBRTC_SRC:-}" +TARGET_OS="${TARGET_OS:-ios}" +JOBS="${JOBS:-8}" +SHALLOW="${SHALLOW:-1}" +RUN_HOOKS="${RUN_HOOKS:-1}" +WEBRTC_REPO="${WEBRTC_REPO:-git@github.com:GetStream/webrtc.git}" +WEBRTC_REVISION="${WEBRTC_REVISION:-}" +WEBRTC_REF="${WEBRTC_REF:-}" + +ensure_src() { + [[ -n "$DEPS_ROOT" ]] || die "DEPS_ROOT is required" + local dest="${DEPS_ROOT}/src" + if [[ -L "$dest" ]]; then + die "src is a symlink ($dest). run: make bootstrap" + fi + if [[ -f "$dest/DEPS" ]] && git -C "$dest" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + return 0 + fi + die "$dest is not this git checkout. run: make bootstrap" +} + +cmd_sync() { + require_cmd gclient + require_cmd git + require_cmd python3 + ensure_src + + if [[ -n "$WEBRTC_REVISION" || -n "$WEBRTC_REF" ]]; then + die "src is seeded from your git checkout; will not reset it" + fi + echo "src is already present; will not reset it" + GIT_CACHE_PATH="${GIT_CACHE_PATH:-$DEPS_ROOT/.gclient-git-cache}" + mkdir -p "$GIT_CACHE_PATH" + export GIT_CACHE_PATH + rewrite_git_cache_alternates "${WEBRTC_SRC:-$DEPS_ROOT/src}" "$GIT_CACHE_PATH" + write_gclient "$DEPS_ROOT" "$WEBRTC_REPO" "$TARGET_OS" + + ( + cd "$DEPS_ROOT" + gclient root >/dev/null || true + local sync=(gclient sync -j"${JOBS}") + if [[ "$SHALLOW" == "1" || "$SHALLOW" == "true" ]]; then + sync+=(--no-history --shallow) + fi + if [[ "$RUN_HOOKS" == "0" || "$RUN_HOOKS" == "false" ]]; then + sync+=(--nohooks) + fi + echo "running: ${sync[*]}" + "${sync[@]}" + ) +} + +cmd_runhooks() { + require_cmd gclient + ensure_src + [[ -f "$DEPS_ROOT/.gclient" ]] || die "gclient config not found at $DEPS_ROOT/.gclient" + ( + cd "$DEPS_ROOT" + echo "running: gclient runhooks" + gclient runhooks + ) +} + +case "${1:-}" in + sync) cmd_sync ;; + runhooks) cmd_runhooks ;; + -h|--help|help) usage ;; + *) usage >&2; exit 1 ;; +esac diff --git a/stream_build/scripts/gn-gen.sh b/stream_build/scripts/gn-gen.sh new file mode 100755 index 0000000000..413b8a084d --- /dev/null +++ b/stream_build/scripts/gn-gen.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Write args.gn and run gn gen. Also prints composed args or a slice ninja target. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +usage() { + cat <<'EOF' +usage: gn-gen.sh --src DIR --out DIR [options] + gn-gen.sh --print [options] + gn-gen.sh --ninja-target SLICE + +options: + --config release|debug default: release + --slice NAME lookup gn/slices.tsv + --overlay NAME gn/NAME.args (repeatable) + --extra "k=v k2=v2" extra GN args + --flat with --print, emit key=value tokens +EOF +} + +SRC="" +OUT_DIR="" +CONFIG="release" +SLICE="" +EXTRA="" +PRINT=0 +FLAT=0 +NINJA_ONLY="" +OVERLAYS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --src) SRC="$2"; shift 2 ;; + --out) OUT_DIR="$2"; shift 2 ;; + --config) CONFIG="$2"; shift 2 ;; + --slice) SLICE="$2"; shift 2 ;; + --overlay) OVERLAYS+=("$2"); shift 2 ;; + --extra) EXTRA="${2:-}"; shift 2 ;; + --print) PRINT=1; shift ;; + --flat) FLAT=1; shift ;; + --ninja-target) NINJA_ONLY="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "gn-gen.sh: unknown flag $1" ;; + esac +done + +if [[ -n "$NINJA_ONLY" ]]; then + slice_ninja_target "$NINJA_ONLY" + exit 0 +fi + +compose_flags=(--config "$CONFIG") +[[ -n "$SLICE" ]] && compose_flags+=(--slice "$SLICE") +[[ -n "$EXTRA" ]] && compose_flags+=(--extra "$EXTRA") +for overlay in "${OVERLAYS[@]+"${OVERLAYS[@]}"}"; do + compose_flags+=(--overlay "$overlay") +done + +args_text="$(compose_gn_args "${compose_flags[@]}")" + +if [[ "$PRINT" -eq 1 ]]; then + if [[ "$FLAT" -eq 1 ]]; then + printf '%s\n' "$args_text" | flatten_gn_args + else + printf '%s\n' "$args_text" + fi + exit 0 +fi + +[[ -n "$SRC" && -n "$OUT_DIR" ]] || die "gn-gen.sh requires --src and --out (or --print)" +require_webrtc_src "$SRC" + +mkdir -p "$OUT_DIR" +printf '%s\n' "$args_text" >"${OUT_DIR}/args.gn" + +gn_bin="$(resolve_gn "$SRC")" +[[ -n "$gn_bin" ]] || die "gn not found" +echo "gn gen ${OUT_DIR}" +( + cd "$SRC" + "$gn_bin" gen "$OUT_DIR" +) diff --git a/stream_build/scripts/macos-gtest-filter.txt b/stream_build/scripts/macos-gtest-filter.txt new file mode 100644 index 0000000000..4651c92f6a --- /dev/null +++ b/stream_build/scripts/macos-gtest-filter.txt @@ -0,0 +1 @@ +*DeathTest*:ThreadTest.TwoThreadsInvokeDeathTest:ThreadTest.ThreeThreadsInvokeDeathTest:BitstreamReaderTest.InDebugModeRequiresToCheckOkStatusBeforeDestruction:BitstreamReaderTest.InDebugModeMayCheckRemainingBitsInsteadOfOkStatus:UnitBaseTest.CrashesWhenCreatedFromNan:AlwaysValidPointerTest.NoDefaultObjectPassNullPointer:AlwaysValidPointerTest.NoDefaultObjectPassNullUniquePointer:FieldTrialsTest.FieldTrialsDoesNotSupportSimultaneousInstances:CustomAudioProcessingTest.NullptrAudioProcessingIsUnsupported:FlatMap.AtFunction:*/FlatTreeTest/*.EraseEndDeath:NetworkTest.DefaultLocalAddress diff --git a/stream_build/scripts/package-android.sh b/stream_build/scripts/package-android.sh new file mode 100755 index 0000000000..acf52dc005 --- /dev/null +++ b/stream_build/scripts/package-android.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Pack already-built Android ABI dirs into libwebrtc.aar. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +require_linux +require_cmd python3 + +SRC="" +OUT="" +PRODUCTS="" +SLICES="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --src) SRC="$2"; shift 2 ;; + --out) OUT="$2"; shift 2 ;; + --products) PRODUCTS="$2"; shift 2 ;; + --slices) SLICES="$2"; shift 2 ;; + *) die "package-android.sh: unknown flag $1" ;; + esac +done + +[[ -n "$SRC" && -n "$OUT" && -n "$PRODUCTS" && -n "$SLICES" ]] || \ + die "package-android.sh requires --src --out --products --slices" + +manifest="$SRC/sdk/android/AndroidManifest.xml" +[[ -f "$manifest" ]] || die "missing $manifest" + +arch_from_slice() { + printf '%s\n' "${1#android-}" +} + +first="" +# shellcheck disable=SC2086 +for slice in $SLICES; do + dir="$OUT/$slice" + [[ -d "$dir" ]] || die "missing build output $dir (run: make build android)" + if [[ -z "$first" ]]; then + first="$slice" + fi +done + +jar="$OUT/$first/lib.java/sdk/android/libwebrtc.jar" +[[ -f "$jar" ]] || die "missing classes jar at $jar" + +mkdir -p "$PRODUCTS" +out_aar="$PRODUCTS/libwebrtc.aar" +rm -f "$out_aar" + +python3 - "$out_aar" "$manifest" "$jar" "$OUT" $SLICES <<'PY' +import os +import sys +import zipfile + +out_aar, manifest, jar, out_root, *slices = sys.argv[1:] +so_name = "libjingle_peerconnection_so.so" + +with zipfile.ZipFile(out_aar, "w") as aar: + aar.write(manifest, "AndroidManifest.xml") + aar.write(jar, "classes.jar") + for slice in slices: + arch = slice[len("android-"):] + so = os.path.join(out_root, slice, so_name) + if not os.path.isfile(so): + so = os.path.join(out_root, slice, "lib.unstripped", so_name) + if not os.path.isfile(so): + raise SystemExit(f"missing {so_name} in {out_root}/{slice}") + aar.write(so, f"jni/{arch}/{so_name}") +print(f"wrote {out_aar}") +PY + +if [[ "${SKIP_LICENSES:-0}" == 1 ]]; then + echo "skipping license generation (SKIP_LICENSES=1)" +else + require_cmd python3 + license_script="$SRC/tools_webrtc/libs/generate_licenses.py" + [[ -f "$license_script" ]] || die "missing $license_script" + build_dirs=() + # shellcheck disable=SC2086 + for slice in $SLICES; do + [[ -d "$OUT/$slice" ]] && build_dirs+=("$OUT/$slice") + done + [[ ${#build_dirs[@]} -gt 0 ]] || die "no slice out dirs for license generation" + echo "python3 $license_script --target sdk/android:libwebrtc --target sdk/android:libjingle_peerconnection_so $PRODUCTS ${build_dirs[*]}" + python3 "$license_script" \ + --target sdk/android:libwebrtc \ + --target sdk/android:libjingle_peerconnection_so \ + "$PRODUCTS" \ + "${build_dirs[@]}" + echo "wrote ${PRODUCTS}/LICENSE.md" +fi diff --git a/stream_build/scripts/package-apple.sh b/stream_build/scripts/package-apple.sh new file mode 100755 index 0000000000..ccaf5e10a2 --- /dev/null +++ b/stream_build/scripts/package-apple.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# lipo Apple framework slices and emit WebRTC.xcframework. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +require_darwin +require_cmd lipo +require_cmd xcodebuild + +SRC="${WEBRTC_SRC:-}" +OUT="" +PRODUCTS="" +SLICES="" +NAME="WebRTC" +ZIP=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --src) SRC="$2"; shift 2 ;; + --out) OUT="$2"; shift 2 ;; + --products) PRODUCTS="$2"; shift 2 ;; + --slices) SLICES="$2"; shift 2 ;; + --name) NAME="$2"; shift 2 ;; + --zip) ZIP=1; shift ;; + *) die "package-apple.sh: unknown flag $1" ;; + esac +done + +[[ -n "$OUT" && -n "$PRODUCTS" && -n "$SLICES" ]] || die "package-apple.sh requires --out --products --slices" + +framework_in_slice() { + printf '%s/%s/%s.framework\n' "$OUT" "$1" "$NAME" +} + +framework_binary() { + local fw="$1" + local bin="$fw/$NAME" + if [[ -e "$fw/Versions/A/$NAME" ]]; then + printf '%s\n' "$fw/Versions/A/$NAME" + return + fi + if [[ -L "$bin" ]]; then + printf '%s/%s\n' "$fw" "$(readlink "$bin")" + return + fi + printf '%s\n' "$bin" +} + +dsym_binary() { + printf '%s.dSYM/Contents/Resources/DWARF/%s\n' "$1" "$NAME" +} + +lipo_group() { + local dest="$1" + shift + local slices=("$@") + local present=() + local slice fw + for slice in "${slices[@]}"; do + fw="$(framework_in_slice "$slice")" + if [[ -d "$fw" ]]; then + present+=("$slice") + fi + done + if [[ ${#present[@]} -eq 0 ]]; then + return 1 + fi + + mkdir -p "$(dirname "$dest")" + rm -rf "$dest" + cp -R "$(framework_in_slice "${present[0]}")" "$dest" + + local binaries=() + for slice in "${present[@]}"; do + binaries+=("$(framework_binary "$(framework_in_slice "$slice")")") + done + local out_bin + out_bin="$(framework_binary "$dest")" + rm -f "$out_bin" + lipo -create "${binaries[@]}" -output "$out_bin" + + local first_dsym="${OUT}/${present[0]}/${NAME}.dSYM" + if [[ -d "$first_dsym" ]]; then + rm -rf "${dest}.dSYM" + cp -R "$first_dsym" "${dest}.dSYM" + local dsym_bins=() + for slice in "${present[@]}"; do + local dsym="${OUT}/${slice}/${NAME}.dSYM" + [[ -d "$dsym" ]] || continue + dsym_bins+=("$(dsym_binary "$dsym")") + done + if [[ ${#dsym_bins[@]} -gt 0 ]]; then + local out_dsym + out_dsym="$(dsym_binary "${dest}.dSYM")" + rm -f "$out_dsym" + mkdir -p "$(dirname "$out_dsym")" + lipo -create "${dsym_bins[@]}" -output "$out_dsym" + fi + fi + return 0 +} + +contains_slice() { + local needle="$1" + local s + # shellcheck disable=SC2086 + for s in $SLICES; do + [[ "$s" == "$needle" ]] && return 0 + done + return 1 +} + +work="${OUT}/_apple_universal" +rm -rf "$work" +mkdir -p "$work" + +xc_args=(-create-xcframework) +added=0 + +add_framework() { + local fw="$1" + [[ -d "$fw" ]] || return 0 + xc_args+=(-framework "$fw") + if [[ -d "${fw}.dSYM" ]]; then + xc_args+=(-debug-symbols "${fw}.dSYM") + fi + added=1 +} + +if contains_slice ios-arm64-device && lipo_group "${work}/ios-device/${NAME}.framework" ios-arm64-device; then + add_framework "${work}/ios-device/${NAME}.framework" +fi +if { contains_slice ios-arm64-simulator || contains_slice ios-x64-simulator; } && \ + lipo_group "${work}/ios-simulator/${NAME}.framework" ios-arm64-simulator ios-x64-simulator; then + add_framework "${work}/ios-simulator/${NAME}.framework" +fi +if { contains_slice catalyst-arm64 || contains_slice catalyst-x64; } && \ + lipo_group "${work}/catalyst/${NAME}.framework" catalyst-arm64 catalyst-x64; then + add_framework "${work}/catalyst/${NAME}.framework" +fi +if { contains_slice macos-arm64 || contains_slice macos-x64; } && \ + lipo_group "${work}/macos/${NAME}.framework" macos-arm64 macos-x64; then + add_framework "${work}/macos/${NAME}.framework" +fi + +[[ "$added" -eq 1 ]] || die "no Apple frameworks found under $OUT for slices: $SLICES" + +mkdir -p "$PRODUCTS" +rm -rf "${PRODUCTS}/${NAME}.xcframework" +xc_args+=(-output "${PRODUCTS}/${NAME}.xcframework") +echo "xcodebuild ${xc_args[*]}" +xcodebuild "${xc_args[@]}" + +xcframework="${PRODUCTS}/${NAME}.xcframework" +if [[ "${SKIP_LICENSES:-0}" == 1 ]]; then + echo "skipping license generation (SKIP_LICENSES=1)" +else + [[ -n "$SRC" ]] || die "package-apple.sh requires --src (or WEBRTC_SRC) to generate licenses" + require_cmd python3 + license_script="$SRC/tools_webrtc/libs/generate_licenses.py" + [[ -f "$license_script" ]] || die "missing $license_script" + build_dirs=() + # shellcheck disable=SC2086 + for slice in $SLICES; do + [[ -d "$OUT/$slice" ]] && build_dirs+=("$OUT/$slice") + done + [[ ${#build_dirs[@]} -gt 0 ]] || die "no slice out dirs for license generation" + gn_targets=() + has_ios=0 + has_macos=0 + # shellcheck disable=SC2086 + for slice in $SLICES; do + if [[ "$slice" == macos-* ]]; then + has_macos=1 + else + has_ios=1 + fi + done + [[ "$has_ios" -eq 1 ]] && gn_targets+=(--target "//sdk:framework_objc") + [[ "$has_macos" -eq 1 ]] && gn_targets+=(--target "//sdk:mac_framework_objc") + [[ ${#gn_targets[@]} -gt 0 ]] || die "no license GN targets for slices: $SLICES" + echo "python3 $license_script ${gn_targets[*]} $xcframework ${build_dirs[*]}" + python3 "$license_script" "${gn_targets[@]}" "$xcframework" "${build_dirs[@]}" + echo "wrote ${xcframework}/LICENSE.md" +fi + +if [[ "$ZIP" -eq 1 ]]; then + if command -v ditto >/dev/null 2>&1; then + ditto -c -k --sequesterRsrc --keepParent \ + "${PRODUCTS}/${NAME}.xcframework" \ + "${PRODUCTS}/${NAME}.xcframework.zip" + else + ( + cd "$PRODUCTS" + zip --symlinks -r "${NAME}.xcframework.zip" "${NAME}.xcframework" + ) + fi +fi + +echo "wrote ${PRODUCTS}/${NAME}.xcframework" diff --git a/stream_build/scripts/package-windows.sh b/stream_build/scripts/package-windows.sh new file mode 100755 index 0000000000..813fd10761 --- /dev/null +++ b/stream_build/scripts/package-windows.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Copy built Windows libs into PRODUCTS (optional zip). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +require_windows + +OUT="" +PRODUCTS="" +SLICES="" +ZIP=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --out) OUT="$2"; shift 2 ;; + --products) PRODUCTS="$2"; shift 2 ;; + --slices) SLICES="$2"; shift 2 ;; + --zip) ZIP=1; shift ;; + *) die "package-windows.sh: unknown flag $1" ;; + esac +done + +[[ -n "$OUT" && -n "$PRODUCTS" && -n "$SLICES" ]] || \ + die "package-windows.sh requires --out --products --slices" + +mkdir -p "$PRODUCTS/windows" +copied=0 +# shellcheck disable=SC2086 +for slice in $SLICES; do + dir="$OUT/$slice" + [[ -d "$dir" ]] || die "missing build output $dir (run: make build windows)" + dest="$PRODUCTS/windows/$slice" + mkdir -p "$dest" + local_copied=0 + for name in webrtc.lib libwebrtc.a webrtc.dll webrtc.dll.lib; do + if [[ -e "$dir/$name" ]]; then + cp -R "$dir/$name" "$dest/" + local_copied=1 + fi + done + if [[ "$local_copied" -eq 0 ]]; then + die "no webrtc lib in $dir" + fi + copied=1 +done + +[[ "$copied" -eq 1 ]] || die "nothing to package" + +if [[ "$ZIP" -eq 1 ]]; then + ( + cd "$PRODUCTS" + zip -r windows.zip windows + ) +fi + +echo "wrote $PRODUCTS/windows" diff --git a/stream_build/scripts/rename-android.sh b/stream_build/scripts/rename-android.sh new file mode 100755 index 0000000000..59090642a2 --- /dev/null +++ b/stream_build/scripts/rename-android.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Copy libwebrtc.aar into PRODUCTS/renamed/. The Android wrapper publishes +# that filename as-is (Maven coords live in stream-video-android-webrtc). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +NAME="libwebrtc.aar" +SRC="" +DEST_DIR="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --src) SRC="$2"; shift 2 ;; + --dest) DEST_DIR="$2"; shift 2 ;; + *) die "rename-android.sh: unknown flag $1" ;; + esac +done + +[[ -n "$SRC" && -n "$DEST_DIR" ]] || die "rename-android.sh requires --src --dest" +[[ -f "$SRC" ]] || die "no AAR at $SRC" +base="$(basename "$SRC")" +[[ "$base" == "$NAME" ]] || die "expected $NAME, got $base" + +mkdir -p "$DEST_DIR" +src_abs="$(cd "$(dirname "$SRC")" && pwd)/$(basename "$SRC")" +dest_abs="$(cd "$DEST_DIR" && pwd)/$NAME" +[[ "$src_abs" != "$dest_abs" ]] || die "refusing to overwrite input $SRC" + +cp "$src_abs" "$dest_abs" +echo "copied $src_abs -> $dest_abs" +echo "original preserved at $src_abs" diff --git a/stream_build/scripts/rename-apple.sh b/stream_build/scripts/rename-apple.sh new file mode 100755 index 0000000000..c2c1ce3fda --- /dev/null +++ b/stream_build/scripts/rename-apple.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Copy WebRTC.xcframework → StreamWebRTC.xcframework (original untouched). +# Matches GetStream/webrtc fastlane rename_product and +# stream-video-swift-webrtc clone_and_modify_xcframework. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +require_darwin +require_cmd find +require_cmd file +require_cmd plutil +require_cmd install_name_tool + +OLD="WebRTC" +NEW="StreamWebRTC" +SRC="" +DEST_DIR="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --src) SRC="$2"; shift 2 ;; + --dest) DEST_DIR="$2"; shift 2 ;; + *) die "rename-apple.sh: unknown flag $1" ;; + esac +done + +[[ -n "$SRC" && -n "$DEST_DIR" ]] || die "rename-apple.sh requires --src --dest" +[[ -d "$SRC" ]] || die "no xcframework at $SRC" +base="$(basename "$SRC")" +[[ "$base" == "${OLD}.xcframework" ]] || die "expected ${OLD}.xcframework, got $base" + +dest="${DEST_DIR}/${NEW}.xcframework" +src_abs="$(cd "$SRC" && pwd)" +mkdir -p "$DEST_DIR" +dest_parent="$(cd "$DEST_DIR" && pwd)" +dest_abs="${dest_parent}/${NEW}.xcframework" +[[ "$src_abs" != "$dest_abs" ]] || die "refusing to overwrite input $SRC" + +rm -rf "$dest_abs" +cp -R "$src_abs" "$dest_abs" +echo "copied $src_abs -> $dest_abs" + +while IFS= read -r -d '' path; do + name="$(basename "$path")" + case "$name" in + "${OLD}.framework"|"${OLD}.dSYM"|"${OLD}.h"|"$OLD") + dir="$(dirname "$path")" + mv "$path" "${dir}/${name/${OLD}/${NEW}}" + ;; + esac +done < <(find "$dest_abs" -depth -print0) + +while IFS= read -r -d '' file; do + if [[ "$file" == *.plist ]]; then + plutil -convert xml1 "$file" + fi + old_text="$(cat "$file")" + new_text="${old_text//${OLD}/${NEW}}" + if [[ "$old_text" != "$new_text" ]]; then + printf '%s\n' "$new_text" > "$file" + fi +done < <(find "$dest_abs" \( -name Info.plist -o -name module.modulemap \) -print0) + +while IFS= read -r -d '' file; do + old_text="$(cat "$file")" + new_text="${old_text//import <${OLD}/import <${NEW}}" + if [[ "$old_text" != "$new_text" ]]; then + printf '%s\n' "$new_text" > "$file" + fi +done < <(find "$dest_abs" -name '*.h' -print0) + +while IFS= read -r -d '' fw; do + ( + cd "$fw" + if [[ -L "$NEW" ]]; then + old_link="$(readlink "$NEW")" + new_link="${old_link//${OLD}/${NEW}}" + if [[ "$old_link" != "$new_link" ]]; then + rm -f "$NEW" + ln -s "$new_link" "$NEW" + fi + fi + bin="$NEW" + [[ -e "$bin" ]] || continue + if file -b "$bin" | grep -q 'Mach-O'; then + install_name_tool -id "@rpath/${NEW}.framework/${NEW}" "$bin" + fi + ) +done < <(find "$dest_abs" -name "${NEW}.framework" -type d -print0) + +echo "wrote $dest_abs" +echo "original preserved at $src_abs" diff --git a/stream_build/scripts/run-ios-tests.sh b/stream_build/scripts/run-ios-tests.sh new file mode 100755 index 0000000000..c815a9d3cd --- /dev/null +++ b/stream_build/scripts/run-ios-tests.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# Run iOS XCTest wrappers produced by ninja sdk_*unittests. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +require_darwin +require_cmd xcrun +require_cmd xcodebuild +require_cmd python3 +require_cmd vpython3 + +# Chromium's generated wrappers are `#!/usr/bin/env vpython3` and probe +# upward for .vpython3. That works when out/ lives under src/. Stream's +# out/ is a sibling of src/, so the probe never reaches src/.vpython3 +# (psutil / cipd wheels). Point vpython at Chromium's spec explicitly. +vpython_spec="${WEBRTC_SRC:-$(cd "${PIPELINE_DIR}/.." && pwd)}/.vpython3" +[[ -f "$vpython_spec" ]] || die "missing vpython spec: ${vpython_spec}" + +BUILD_DIR="" +TARGETS="" +SIMULATOR_PLATFORM="${SIMULATOR_PLATFORM:-}" +SIMULATOR_VERSION="${SIMULATOR_VERSION:-}" +EXTRA_ARGS="${EXTRA_ARGS:-}" + +while [[ $# -gt 0 ]]; do + case "$1" in + --build-dir) BUILD_DIR="$2"; shift 2 ;; + --targets) TARGETS="$2"; shift 2 ;; + --platform) SIMULATOR_PLATFORM="$2"; shift 2 ;; + --version) SIMULATOR_VERSION="$2"; shift 2 ;; + --extra) EXTRA_ARGS="${2:-}"; shift 2 ;; + *) die "run-ios-tests.sh: unknown flag $1" ;; + esac +done + +[[ -n "$BUILD_DIR" && -n "$TARGETS" ]] || die "run-ios-tests.sh requires --build-dir and --targets" + +# Match the SDK the .app was compiled with. Newest-device auto-select +# picks iOS 27.0 on Xcode 26.6 (SDK 26.5), then Chromium creates a +# simulator on that runtime and xcodebuild cannot launch the runner. +compiled_ios_sdk() { + local target plist ver + # shellcheck disable=SC2086 + for target in $TARGETS; do + plist="${BUILD_DIR}/${target}.app/Info.plist" + [[ -f "$plist" ]] || continue + ver="$(plutil -extract DTPlatformVersion raw "$plist" 2>/dev/null || true)" + if [[ -n "$ver" ]]; then + printf '%s\n' "$ver" + return 0 + fi + done + xcrun --sdk iphonesimulator --show-sdk-version +} + +# Chromium's wrapper bakes --xcode-path ../../src/Xcode.app (CIPD). +# argparse last-wins; point at the selected Xcode so local runs do not +# look for a hermetic tree. install_xcode() no-ops without LUCI_CONTEXT. +selected_xcode_app() { + (cd "$(xcode-select -p)/../.." && pwd) +} + +pick_simulator() { + python3 - "$SIMULATOR_PLATFORM" "$SIMULATOR_VERSION" <<'PY' +import json, subprocess, sys + +want_name, want_version = sys.argv[1], sys.argv[2] +payload = json.loads( + subprocess.check_output(["xcrun", "simctl", "list", "--json"], text=True) +) + + +def matches(runtime_version): + rv, want = runtime_version.strip(), want_version.strip() + return rv == want or rv.startswith(want + ".") or want.startswith(rv + ".") + + +def is_iphone(devicetype): + if devicetype.get("productFamily") == "iPhone": + return True + return (devicetype.get("name") or "").startswith("iPhone") + + +for runtime in payload.get("runtimes") or []: + ident = runtime.get("identifier") or "" + name = runtime.get("name") or "" + if "iOS" not in ident and "iOS" not in name: + continue + if runtime.get("isAvailable") is False: + continue + version = (runtime.get("version") or "").strip() + if want_version and not matches(version): + continue + types = [ + dt.get("name") or "" + for dt in (runtime.get("supportedDeviceTypes") or []) + if is_iphone(dt) + ] + types = [t for t in types if t] + if want_name: + if want_name not in types: + continue + print(f"{want_name}\t{version}") + raise SystemExit(0) + if types: + # ponytail: Apple lists newest iPhones first on Xcode 26.x. + # If that order flips, first-iPhone still matches the SDK. + print(f"{types[0]}\t{version}") + raise SystemExit(0) + +sys.exit( + f"no available iOS simulator runtime matched SDK {want_version or '?'}" +) +PY +} + +if [[ -z "$SIMULATOR_VERSION" ]]; then + SIMULATOR_VERSION="$(compiled_ios_sdk)" +fi +if [[ -z "$SIMULATOR_PLATFORM" ]]; then + selected="$(pick_simulator)" + SIMULATOR_PLATFORM="${selected%%$'\t'*}" + SIMULATOR_VERSION="${selected#*$'\t'}" +fi +echo "using simulator: ${SIMULATOR_PLATFORM} (iOS ${SIMULATOR_VERSION})" + +xcode_build_version="$(xcodebuild -version | awk '/Build version/{print $3; exit}')" +xcode_build_version="${xcode_build_version:-local}" +xcode_app="$(selected_xcode_app)" +out_dir="${BUILD_DIR}/test_output" +rm -rf "$out_dir" +mkdir -p "$out_dir" + +run_target() { + local target="$1" + local wrapper="${BUILD_DIR}/bin/run_${target}" + [[ -x "$wrapper" ]] || die "run script not found for ${target} at ${wrapper}" + local args=( + --xctest + --out-dir "$out_dir" + --xcode-build-version "$xcode_build_version" + --xcode-path "$xcode_app" + --platform "$SIMULATOR_PLATFORM" + --version "$SIMULATOR_VERSION" + ) + # shellcheck disable=SC2086 + if [[ -n "$EXTRA_ARGS" ]]; then + # shellcheck disable=SC2206 + args+=($EXTRA_ARGS) + fi + echo "running ${wrapper} ${args[*]}" + if vpython3 -vpython-spec "$vpython_spec" "$wrapper" "${args[@]}"; then + return 0 + fi + if grep -Rqs "Test Suite 'All tests' passed\|Test Suite 'Selected tests' passed" "$out_dir"; then + echo "iOS test wrapper reported failure after XCTest already passed; treating as success" + return 0 + fi + return 1 +} + +# shellcheck disable=SC2086 +for target in $TARGETS; do + run_target "$target" +done diff --git a/stream_build/webrtc.mk b/stream_build/webrtc.mk new file mode 100644 index 0000000000..070d265f91 --- /dev/null +++ b/stream_build/webrtc.mk @@ -0,0 +1,20 @@ +# webrtc/Makefile — created by bootstrap if missing +# Catch-all forwarder. Recipes live in src/stream_build (cwd must be there). + +MAKECMDGOALS ?= +STREAM_BUILD := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))src/stream_build +GOALS := $(filter-out all,$(MAKECMDGOALS)) +FIRST := $(firstword $(or $(MAKECMDGOALS),all)) +REST := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) + +.DEFAULT_GOAL := all +.PHONY: all $(MAKECMDGOALS) + +# One recursive make for `make build ios`; extra goals are no-ops. +$(FIRST): + @$(MAKE) -C "$(STREAM_BUILD)" $(GOALS) + +ifneq ($(REST),) +$(REST): + @: +endif