From 69dc456c271870307e414e1da64ea8a7f78d7fd2 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Sun, 12 Jul 2026 13:53:16 +0200 Subject: [PATCH] feat(scripts): docker CI image, runner submodule seed, OTA browser apk Signed-off-by: Anton Afanasyeu --- Dockerfile.androidcast-ci | 42 ++++++++++++ build.config.yml | 20 ++++++ docker-build-runner.sh | 137 ++++++++++++++++++++++++++++++++++++-- generate-ota-v0.sh | 4 ++ 4 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 Dockerfile.androidcast-ci create mode 100644 build.config.yml diff --git a/Dockerfile.androidcast-ci b/Dockerfile.androidcast-ci new file mode 100644 index 0000000..72041d1 --- /dev/null +++ b/Dockerfile.androidcast-ci @@ -0,0 +1,42 @@ +# Android Cast CI image — Gradle + NDK + native codec build deps. +# Build: docker build -f ac-scripts/Dockerfile.androidcast-ci -t androidcast-ci:latest ac-scripts +FROM ubuntu:22.04 + +ARG ANDROIDCAST_CI_VERSION=00.01.00.1000 +ENV DEBIAN_FRONTEND=noninteractive \ + ANDROID_HOME=/opt/android-sdk \ + ANDROID_SDK_ROOT=/opt/android-sdk \ + JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 \ + GRADLE_USER_HOME=/root/.gradle \ + ANDROIDCAST_CI_VERSION=${ANDROIDCAST_CI_VERSION} + +RUN apt-get update && apt-get install -y --no-install-recommends \ + openjdk-21-jdk-headless \ + git bash curl unzip wget ca-certificates \ + autoconf automake libtool pkg-config make g++ python3 \ + ccache \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p "${ANDROID_HOME}/cmdline-tools" \ + && curl -fsSL https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -o /tmp/cmdline-tools.zip \ + && unzip -q /tmp/cmdline-tools.zip -d "${ANDROID_HOME}/cmdline-tools" \ + && mv "${ANDROID_HOME}/cmdline-tools/cmdline-tools" "${ANDROID_HOME}/cmdline-tools/latest" \ + && rm /tmp/cmdline-tools.zip + +ENV PATH="${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}" + +RUN yes | sdkmanager --licenses >/dev/null \ + && sdkmanager --install \ + "platform-tools" \ + "platforms;android-34" \ + "build-tools;34.0.0" \ + "ndk;29.0.14206865" \ + "cmake;3.22.1" + +ENV ANDROID_NDK_HOME="${ANDROID_HOME}/ndk/29.0.14206865" \ + ANDROID_NDK_ROOT="${ANDROID_HOME}/ndk/29.0.14206865" + +WORKDIR /workspace +LABEL org.androidcast.ci.version="${ANDROIDCAST_CI_VERSION}" + +CMD ["./scripts/ci-build-and-publish-ota.sh"] diff --git a/build.config.yml b/build.config.yml new file mode 100644 index 0000000..e6ea9fd --- /dev/null +++ b/build.config.yml @@ -0,0 +1,20 @@ +# Canonical CI pipeline for ac-ms-build / docker-build-runner.sh (cluster lab). +# Referenced from compose-lab-backend.php → build.pipeline_config +version: 2.1 +parameters: + git_ref: next + gradle_task: assembleDebug + run_tests: true + run_native: true + run_apk: true + auto_ota: true + ota_channel: staging +jobs: + android-build: + docker: + - image: androidcast-ci:00.01.00.1000 + steps: + - checkout + - run: + name: Gradle pipeline + command: ./scripts/ci-build-and-publish-ota.sh diff --git a/docker-build-runner.sh b/docker-build-runner.sh index b12ad92..9727f38 100755 --- a/docker-build-runner.sh +++ b/docker-build-runner.sh @@ -13,6 +13,7 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" IMAGE="${ANDROIDCAST_CI_IMAGE:-androidcast-ci:latest}" CI_VERSION="${ANDROIDCAST_CI_VERSION:-00.01.00.1000}" BUILD_ID="${BUILD_ID:-local}" @@ -22,6 +23,9 @@ OUT_DIR="${BUILD_OUT_DIR:-${WORK_DIR}/out/builds/${BUILD_ID}}" LOG_STDOUT_ONLY="${BUILD_LOG_STDOUT_ONLY:-0}" ISOLATE_SOURCE="${BUILD_ISOLATE_SOURCE:-1}" CLONE_DEPTH="${BUILD_GIT_DEPTH:-1}" +MOBILE_SUBDIR="${BUILD_MOBILE_SUBDIR:-ac-mobile-android}" +HOST_SCRIPTS_DIR="${BUILD_SCRIPTS_DIR:-${SCRIPTS_DIR}}" +DOCKERFILE_PATH="${BUILD_DOCKERFILE:-}" SOURCE_DIR="${OUT_DIR}/src" DOCKER_WORKSPACE="${WORK_DIR}" CONTAINER_NAME="androidcast-bld-${BUILD_ID}" @@ -93,6 +97,93 @@ resolve_git_remote() { fi } +host_mobile_tree() { + if [[ -d "${WORK_DIR}/${MOBILE_SUBDIR}/app" ]]; then + echo "${WORK_DIR}/${MOBILE_SUBDIR}" + elif [[ -d "${WORK_DIR}/app" ]]; then + echo "${WORK_DIR}" + else + echo "" + fi +} + +isolated_mobile_tree() { + local base="$1" + if [[ -d "${base}/${MOBILE_SUBDIR}/app" ]]; then + echo "${base}/${MOBILE_SUBDIR}" + elif [[ -d "${base}/app" ]]; then + echo "${base}" + else + echo "" + fi +} + +third_party_incomplete() { + local tree="$1" + local p="$2" + local dir="${tree}/third-party/${p}" + if [[ ! -d "$dir" ]]; then + return 0 + fi + case "$p" in + wireguard-android) + [[ ! -d "${dir}/tunnel/src/main/java" ]] + ;; + libvpx) + [[ ! -d "${dir}/vp8" && ! -f "${dir}/configure" ]] + ;; + opus|speex) + [[ ! -f "${dir}/configure" && ! -f "${dir}/CMakeLists.txt" && ! -d "${dir}/include" ]] + ;; + *) + [[ -z "$(ls -A "${dir}" 2>/dev/null)" ]] + ;; + esac +} + +seed_third_party_from_host() { + local host_tree="$1" + local iso_tree="$2" + if [[ -z "$host_tree" || -z "$iso_tree" || ! -d "$host_tree/third-party" ]]; then + return 0 + fi + mkdir -p "${iso_tree}/third-party" + local p + for p in libvpx opus speex wireguard-android; do + if [[ -d "${host_tree}/third-party/${p}" ]] && third_party_incomplete "$iso_tree" "$p"; then + log "[runner] seed third-party/${p} from host checkout" + rm -rf "${iso_tree}/third-party/${p}" + cp -r "${host_tree}/third-party/${p}" "${iso_tree}/third-party/" + fi + done +} + +stage_ci_scripts() { + local iso_tree="$1" + if [[ -z "$iso_tree" || ! -d "$HOST_SCRIPTS_DIR" ]]; then + return 0 + fi + mkdir -p "${iso_tree}/scripts" + local f + for f in ci-build-and-publish-ota.sh init-third-party-submodules.sh build-native-codecs.sh android-ndk.sh generate-ota-v0.sh native-build-cache.sh; do + if [[ -f "${HOST_SCRIPTS_DIR}/${f}" ]]; then + cp -f "${HOST_SCRIPTS_DIR}/${f}" "${iso_tree}/scripts/${f}" + chmod +x "${iso_tree}/scripts/${f}" 2>/dev/null || true + fi + done +} + +init_mobile_submodule_only() { + local base="$1" + if [[ ! -d "${base}/.git" ]]; then + return 0 + fi + if git -C "$base" config -f .gitmodules --get "submodule.${MOBILE_SUBDIR}.path" >/dev/null 2>&1; then + log "[runner] isolate: init workspace submodule ${MOBILE_SUBDIR} (no third-party recurse)" + run_step git-submodule-mobile git -c safe.directory="${base}" -C "${base}" submodule update --init "${MOBILE_SUBDIR}" + fi +} + prepare_isolated_source() { local ref="${GIT_REF:-}" local remote sha @@ -105,14 +196,13 @@ prepare_isolated_source() { rm -rf "${SOURCE_DIR}" mkdir -p "${SOURCE_DIR}" - log "[runner] isolate: shallow clone depth=${CLONE_DEPTH} ref=${ref} remote=${remote} -> ${SOURCE_DIR}" - if git clone --depth "${CLONE_DEPTH}" --recurse-submodules --shallow-submodules \ + log "[runner] isolate: shallow clone depth=${CLONE_DEPTH} ref=${ref} remote=*** -> ${SOURCE_DIR}" + if git clone --depth "${CLONE_DEPTH}" \ --single-branch --branch "${ref}" "${remote}" "${SOURCE_DIR}"; then : else log "[runner] isolate: branch clone failed; clone default branch then checkout ${ref}" - run_step git-clone git clone --depth "${CLONE_DEPTH}" --recurse-submodules --shallow-submodules \ - "${remote}" "${SOURCE_DIR}" + run_step git-clone git clone --depth "${CLONE_DEPTH}" "${remote}" "${SOURCE_DIR}" run_step git-fetch git -c safe.directory="${SOURCE_DIR}" -C "${SOURCE_DIR}" fetch --depth "${CLONE_DEPTH}" origin "${ref}" run_step git-checkout git -c safe.directory="${SOURCE_DIR}" -C "${SOURCE_DIR}" checkout "${ref}" fi @@ -122,8 +212,20 @@ prepare_isolated_source() { run_step git-pin git -c safe.directory="${SOURCE_DIR}" -C "${SOURCE_DIR}" checkout "${GIT_SHA}" fi + init_mobile_submodule_only "${SOURCE_DIR}" + + local host_tree iso_tree + host_tree="$(host_mobile_tree)" + iso_tree="$(isolated_mobile_tree "${SOURCE_DIR}")" + if [[ -n "$iso_tree" ]]; then + seed_third_party_from_host "$host_tree" "$iso_tree" + stage_ci_scripts "$iso_tree" + DOCKER_WORKSPACE="${iso_tree}" + else + DOCKER_WORKSPACE="${SOURCE_DIR}" + fi + run_step git-rev-parse git -c safe.directory="${SOURCE_DIR}" -C "${SOURCE_DIR}" rev-parse HEAD - DOCKER_WORKSPACE="${SOURCE_DIR}" } sync_shared_checkout() { @@ -212,11 +314,31 @@ log "[runner] docker workspace=${DOCKER_WORKSPACE}" if should_skip_docker_build; then log "[runner] resume: skipped docker-build (image ${IMAGE} exists)" else + dockerfile="${DOCKERFILE_PATH}" + if [[ -z "$dockerfile" || ! -f "$dockerfile" ]]; then + for candidate in \ + "${HOST_SCRIPTS_DIR}/Dockerfile.androidcast-ci" \ + "${DOCKER_WORKSPACE}/Dockerfile" \ + "${ROOT}/Dockerfile"; do + if [[ -f "$candidate" ]]; then + dockerfile="$candidate" + break + fi + done + fi + if [[ -z "$dockerfile" || ! -f "$dockerfile" ]]; then + log "[runner] ERROR: CI Dockerfile not found (set BUILD_DOCKERFILE or add Dockerfile.androidcast-ci under ac-scripts)" + exit 1 + fi + docker_ctx="${DOCKER_WORKSPACE}" + if [[ "$(basename "$dockerfile")" == "Dockerfile.androidcast-ci" ]]; then + docker_ctx="$(dirname "$dockerfile")" + fi run_step docker-build docker build \ --build-arg ANDROIDCAST_CI_VERSION="${CI_VERSION}" \ -t "$IMAGE" \ - -f "${ROOT}/Dockerfile" \ - "${ROOT}" + -f "${dockerfile}" \ + "${docker_ctx}" fi if [[ "$SSH_DEBUG" == "1" ]]; then @@ -225,6 +347,7 @@ if [[ "$SSH_DEBUG" == "1" ]]; then fi run_step docker-run docker run --rm --name "${CONTAINER_NAME}" \ + --network host \ -v "${DOCKER_WORKSPACE}:/workspace" \ -v "${ROOT}/out:/workspace/out" \ -w /workspace \ diff --git a/generate-ota-v0.sh b/generate-ota-v0.sh index 95b14a3..1371302 100755 --- a/generate-ota-v0.sh +++ b/generate-ota-v0.sh @@ -76,7 +76,10 @@ rel_dir="${rel_root}/00/${maj_p}/${ver_p}" mkdir -p "${rel_dir}" "${rel_root}/channel" pkg_name="android_cast_${ver_p}.${bld_p}.otapkg" +browser_pkg_name="android_cast_${ver_p}.${bld_p}.apk" +browser_apk_url="${artifact_base}.apk" cp -f "$apk" "${rel_dir}/${pkg_name}" +cp -f "$apk" "${rel_dir}/${browser_pkg_name}" sign_name="android_cast_${ver_p}.${bld_p}_sign.json" manifest_name="android_cast_${ver_p}.${bld_p}_manifest.json" @@ -123,6 +126,7 @@ cat >"${rel_dir}/${manifest_name}" <