#!/usr/bin/env bash # # SPDX-License-Identifier: GPL-2.0 # # Copyright (c) 2026 AMLOGIC # # Chromium browser build script for AMLbian # Hardware video decoding (V4L2) + DMA-BUF zero-copy # # Controlled by BUILD_UNITS: add "chromium" to enable # NOTE: set -e is NOT used here. This script modifies PATH/env and runs in a # subshell (line ~450) to isolate side-effects from the parent build system. # Error handling is explicit (|| return 1) throughout. # ============================================================================== # Section 1: Default Configuration # ============================================================================== CHROMIUM_VERSION="132.0.6834.83" # -amlN: bump whenever the package content changes DEB_VERSION="${CHROMIUM_VERSION}-aml2" DEB_NAME="chromium-browser-aml" # Get script directory (scripts/chromium/ — two levels below AMLBIAN_DIR) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" AMLBIAN_DIR="$(dirname "$(dirname "${SCRIPT_DIR}")")" PATCHES_DIR="${SCRIPT_DIR}/patches" OVERLAY_DIR="${SCRIPT_DIR}/overlay" CONFIG_DIR="${SCRIPT_DIR}/config" : "${CHROMIUM_BUILD_ROOT:=${AMLBIAN_DIR}/../chromium}" : "${DEPOT_TOOLS_DIR:=${CHROMIUM_BUILD_ROOT}/depot_tools}" # CHROMIUM_DL_DIR: optional env var (Jenkins node config or manual). # Tarball search order: CHROMIUM_DL_DIR → CHROMIUM_MIRROR_DIR → local cache → CDN. : "${CHROMIUM_MIRROR_DIR:=/mnt/fileroot/Public/mirror}" CHROMIUM_SRC="${CHROMIUM_BUILD_ROOT}/chromium-${CHROMIUM_VERSION}" OUT_DIR="${CHROMIUM_SRC}/out/arm64" # Armbian sets SRC to the build dir. BUILD_DEBS must match ${SRC}/output/ # which is where install_aml_deb looks for debs in Docker. if [[ -n "${SRC}" ]] && [[ -d "${SRC}/output" ]]; then BUILD_DEBS="${SRC}/output" else BUILD_DEBS="${AMLBIAN_DIR}/../output" fi : "${NINJA_JOBS:=$(nproc)}" # Fallback display_alert for standalone testing if ! command -v display_alert &>/dev/null; then display_alert() { echo "[$2] $1 ${3:+($3)}"; } fi # ============================================================================== # Section 2: Internal Helper # ============================================================================== # Find a file in multiple directories. Sets REPLY to the first hit. function __chromium_find_in_dirs() { local filename="$1"; shift REPLY="" local dir for dir in "$@"; do [[ -z "${dir}" ]] && continue if [[ -f "${dir}/${filename}" ]]; then REPLY="${dir}/${filename}" return 0 fi done return 1 } # ============================================================================== # Section 3: Build Functions # ============================================================================== function __chromium_check_enabled() { # When running standalone (no BUILD_UNITS), always build if ! command -v is_unit_enabled &>/dev/null; then return 0 fi if ! is_unit_enabled "chromium"; then display_alert "CHROMIUM" "Not in BUILD_UNITS, skipping" "info" return 1 fi return 0 } function __chromium_fetch_source() { display_alert "CHROMIUM" "Fetching source ${CHROMIUM_VERSION}" "info" mkdir -p "${CHROMIUM_BUILD_ROOT}" # If source dir exists but is incomplete (any state: empty, only out/, partial), # remove it so we get a clean extraction. if [[ -d "${CHROMIUM_SRC}" ]] && [[ ! -f "${CHROMIUM_SRC}/.gn" ]]; then display_alert "CHROMIUM" "Incomplete source tree at ${CHROMIUM_SRC} — removing" "wrn" rm -rf "${CHROMIUM_SRC}" fi # Source already complete if [[ -f "${CHROMIUM_SRC}/.gn" ]]; then display_alert "CHROMIUM" "Source complete at ${CHROMIUM_SRC}" "info" return 0 fi # Find tarball: CHROMIUM_DL_DIR (Jenkins) → mirror → local cache local tarball="chromium-${CHROMIUM_VERSION}.tar.xz" local tarball_path="" if __chromium_find_in_dirs "${tarball}" \ "${CHROMIUM_DL_DIR}" \ "${CHROMIUM_MIRROR_DIR}" \ "${CHROMIUM_BUILD_ROOT}"; then tarball_path="${REPLY}" display_alert "CHROMIUM" "Found tarball: ${tarball_path}" "info" else # Last resort: download from Google CDN tarball_path="${CHROMIUM_BUILD_ROOT}/${tarball}" display_alert "CHROMIUM" "Downloading ${tarball} from Google CDN (6.4 GB) ..." "info" if ! wget -q --show-progress \ "https://commondatastorage.googleapis.com/chromium-browser-official/${tarball}" \ -O "${tarball_path}"; then display_alert "CHROMIUM" "ERROR: download failed" "err" rm -f "${tarball_path}" return 1 fi fi display_alert "CHROMIUM" "Extracting ${tarball_path} ..." "info" if ! tar -xf "${tarball_path}" -C "${CHROMIUM_BUILD_ROOT}"; then display_alert "CHROMIUM" "ERROR: tar extraction failed" "err" rm -rf "${CHROMIUM_SRC}" return 1 fi if [[ ! -f "${CHROMIUM_SRC}/.gn" ]]; then display_alert "CHROMIUM" "ERROR: .gn missing after extraction (CHROMIUM_SRC=${CHROMIUM_SRC})" "err" return 1 fi display_alert "CHROMIUM" "Source ready at ${CHROMIUM_SRC}" "info" } function __chromium_apply_patches() { display_alert "CHROMIUM" "Applying patches" "info" cd "${CHROMIUM_SRC}" || { display_alert "CHROMIUM" "ERROR: Cannot cd to ${CHROMIUM_SRC}" "err"; return 1; } # Apply patches in order (patch -p1, no git repo); -F0 -f: exact context only, never auto-reverse. # Only "already applied" is skipped; a missing or non-applying patch fails the build. for p in minimal-hwdecode-final.patch wayland-disable-implicit-sync-interop.patch \ mpris-skip-infinite-duration-position.patch; do if patch -p1 -F0 -f --dry-run -s < "${PATCHES_DIR}/${p}" > /dev/null 2>&1; then patch -p1 -F0 -f < "${PATCHES_DIR}/${p}" || \ { display_alert "CHROMIUM" "ERROR: patch failed: ${p}" "err"; return 1; } display_alert "CHROMIUM" "Patch applied: ${p}" "info" elif patch -p1 -R -F0 -f --dry-run -s < "${PATCHES_DIR}/${p}" > /dev/null 2>&1; then display_alert "CHROMIUM" "Patch already applied, skipping: ${p}" "info" else display_alert "CHROMIUM" "ERROR: patch does not apply: ${p}" "err" patch -p1 -F0 -f --dry-run < "${PATCHES_DIR}/${p}" 2>&1 | head -40 || true return 1 fi done # Copy overlay files if [[ -d "${OVERLAY_DIR}" ]]; then cp -rv "${OVERLAY_DIR}/." "${CHROMIUM_SRC}/" display_alert "CHROMIUM" "Overlay applied" "info" fi } function __chromium_configure() { display_alert "CHROMIUM" "Configuring build" "info" # Force system Python — depot_tools bundled Python has wrong prefix (/work/out) export PATH="/usr/bin:${DEPOT_TOOLS_DIR}:${PATH}" unset PYTHONHOME PYTHONPATH export GN_ARGS_USE_SYSTEM_PYTHON=1 export VPYTHON_BYPASS="manually managed vpython not supported by nickel" export DEPOT_TOOLS_UPDATE=0 cd "${CHROMIUM_SRC}" || { display_alert "CHROMIUM" "ERROR: Cannot cd to ${CHROMIUM_SRC}" "err" return 1 } # Resolve canonical path — CHROMIUM_SRC may contain ".." when sourced by Armbian # (e.g. amlbian/../chromium/...). gn fails to find .gn with ".." in OUT_DIR. local src_abs src_abs="$(pwd)" local out_abs="${src_abs}/out/arm64" if [[ ! -f "${src_abs}/.gn" ]]; then display_alert "CHROMIUM" "ERROR: .gn not found at ${src_abs} — source tree missing" "err" return 1 fi # Toolchains: clang, rust, sysroot. # The source tarball doesn't include these. Strategy: # 1. Reuse from existing build tree at ${CHROMIUM_BUILD_ROOT}/src/ (same chromium version) # 2. Download from Google CDN via update scripts local existing_src="${CHROMIUM_BUILD_ROOT}/src" local clang_dir="${src_abs}/third_party/llvm-build" local rust_dir="${src_abs}/third_party/rust-toolchain" local sysroot_dir="${src_abs}/build/linux/debian_bullseye_arm64-sysroot" # Expected clang revision (used to verify correct version) local expected_clang_rev expected_clang_rev=$(grep "^CLANG_REVISION = " "${src_abs}/tools/clang/scripts/update.py" | cut -d"'" -f2) # --- Clang --- if ! grep -q "${expected_clang_rev}" "${clang_dir}/Release+Asserts/cr_build_revision" 2>/dev/null; then if [[ -d "${existing_src}/third_party/llvm-build/Release+Asserts/bin" ]] && \ grep -q "${expected_clang_rev}" "${existing_src}/third_party/llvm-build/Release+Asserts/cr_build_revision" 2>/dev/null; then display_alert "CHROMIUM" "Linking clang from existing build" "info" rm -rf "${clang_dir}" ln -snf "${existing_src}/third_party/llvm-build" "${clang_dir}" else display_alert "CHROMIUM" "Downloading clang via update.py" "info" python3 "${src_abs}/tools/clang/scripts/update.py" || { display_alert "CHROMIUM" "ERROR: clang setup failed" "err"; return 1; } fi fi # --- Rust --- if [[ ! -x "${rust_dir}/bin/rustc" ]]; then if [[ -x "${existing_src}/third_party/rust-toolchain/bin/rustc" ]]; then display_alert "CHROMIUM" "Linking rust from existing build" "info" rm -rf "${rust_dir}" ln -snf "${existing_src}/third_party/rust-toolchain" "${rust_dir}" else display_alert "CHROMIUM" "Downloading rust via update_rust.py" "info" python3 "${src_abs}/tools/rust/update_rust.py" || { display_alert "CHROMIUM" "ERROR: rust setup failed" "err"; return 1; } fi fi # --- Sysroot (arm64 cross-compilation) --- if [[ ! -d "${sysroot_dir}" ]]; then if [[ -d "${existing_src}/build/linux/debian_bullseye_arm64-sysroot" ]]; then display_alert "CHROMIUM" "Linking arm64 sysroot from existing build" "info" ln -snf "${existing_src}/build/linux/debian_bullseye_arm64-sysroot" "${sysroot_dir}" else display_alert "CHROMIUM" "Installing arm64 sysroot via install-sysroot.py" "info" python3 "${src_abs}/build/linux/sysroot_scripts/install-sysroot.py" --arch=arm64 || { display_alert "CHROMIUM" "ERROR: sysroot installation failed" "err"; return 1; } fi fi mkdir -p "${out_abs}" cp "${CONFIG_DIR}/args.gn" "${out_abs}/args.gn" local gn_bin="${src_abs}/buildtools/linux64/gn" if [[ ! -x "${gn_bin}" ]]; then display_alert "CHROMIUM" "ERROR: gn not found at ${gn_bin}" "err" return 1 fi "${gn_bin}" gen "${out_abs}" || { display_alert "CHROMIUM" "ERROR: gn gen failed" "err" return 1 } display_alert "CHROMIUM" "Configured: ${out_abs}" "info" } function __chromium_compile() { display_alert "CHROMIUM" "Building (jobs=${NINJA_JOBS})" "info" cd "${CHROMIUM_SRC}" || { display_alert "CHROMIUM" "ERROR: Cannot cd to ${CHROMIUM_SRC}" "err"; return 1; } local src_abs src_abs="$(pwd)" "${src_abs}/third_party/ninja/ninja" -C "${src_abs}/out/arm64" -j "${NINJA_JOBS}" chrome || { display_alert "CHROMIUM" "ERROR: ninja build failed" "err" return 1 } display_alert "CHROMIUM" "Build complete" "info" } function __chromium_package_deb() { display_alert "CHROMIUM" "Packaging ${DEB_NAME} ${DEB_VERSION}" "info" mkdir -p "${BUILD_DEBS}" local pkg_root="${BUILD_DEBS}/.chromium-pkg-$$" rm -rf "${pkg_root}" mkdir -p "${pkg_root}" local install_dir="${pkg_root}/usr/lib/chromium" local bin_dir="${pkg_root}/usr/bin" local app_dir="${pkg_root}/usr/share/applications" local limits_dir="${pkg_root}/etc/security/limits.d" local profile_dir="${pkg_root}/etc/profile.d" mkdir -p "${install_dir}/locales" "${bin_dir}" "${app_dir}" \ "${limits_dir}" "${profile_dir}" local src src="$(realpath "${OUT_DIR}")" display_alert "CHROMIUM" "Packaging from: ${src}" "info" # Binary + resources if [[ ! -f "${src}/chrome" ]]; then display_alert "CHROMIUM" "ERROR: ${src}/chrome not found!" "err" return 1 fi install -m 755 "${src}/chrome" "${install_dir}/chrome" for f in "${src}"/icudtl.dat "${src}"/*.pak "${src}"/*.bin; do [[ -f "${f}" ]] && install -m 644 "${f}" "${install_dir}/" done # ANGLE libraries (required for GPU process) for f in "${src}"/libGLESv2.so "${src}"/libEGL.so; do [[ -f "${f}" ]] && install -m 644 "${f}" "${install_dir}/" done # Crashpad handler [[ -f "${src}/chrome_crashpad_handler" ]] && \ install -m 755 "${src}/chrome_crashpad_handler" "${install_dir}/" [[ -f "${src}/locales/en-US.pak" ]] && \ install -m 644 "${src}/locales/en-US.pak" "${install_dir}/locales/" # Launcher. The fence wait switch (HasSwitch() in patches/wayland-disable-implicit-sync-interop.patch) # fixes tearing on Mali. Video drops: no SingleVideoFrameRateThrottling, 3 buffers instead of # --double-buffer-compositing. HandleChildThreadTypeChangesInBrowser: see chromium-nice.conf below. cat > "${bin_dir}/chromium-browser" << 'LAUNCHER' #!/bin/sh export PULSE_SERVER=unix:/run/user/1000/pulse/native ulimit -l unlimited 2>/dev/null exec /usr/lib/chromium/chrome \ --ozone-platform=wayland --no-sandbox --use-gl=angle --use-angle=gles \ --ignore-gpu-blocklist --disable-gpu-process-crash-limit \ --disable-wayland-implicit-sync-interop \ --disable-features=WebGPU,SingleVideoFrameRateThrottling,HandleChildThreadTypeChangesInBrowser \ --enable-features=AcceleratedVideoDecodeLinux,AcceleratedVideoDecodeLinuxGL,ChromeOSHWAV1Decoder \ --disable-partial-raster --lang=en-US \ "$@" 2>/tmp/chrome_stderr.log LAUNCHER chmod 755 "${bin_dir}/chromium-browser" # Desktop entry cat > "${app_dir}/chromium-browser.desktop" << 'DESKTOP' [Desktop Entry] Version=1.0 Name=Chromium Browser Exec=chromium-browser %U Terminal=false Type=Application Icon=chromium-browser Categories=Network;WebBrowser; MimeType=text/html;text/xml;application/xhtml+xml; DESKTOP # memlock limit echo "* - memlock unlimited" > "${limits_dir}/chromium-memlock.conf" # Chromium renices its display threads to -8 in each process (platform_thread_linux.cc); the browser # cannot do it for children without CONFIG_PID_NS, hence HandleChildThreadTypeChangesInBrowser off. # Desktop users only (lib/function-img.sh creates the user in the video group). echo "@video - nice -8" > "${limits_dir}/chromium-nice.conf" # HW decode sysfs setup (runs at login) cat > "${profile_dir}/aml-hwdecode.sh" << 'HWDEC' #!/bin/sh HWDEC chmod 644 "${profile_dir}/aml-hwdecode.sh" # Systemd service for DW mode + GPU freq (needs root) local svc_dir="${pkg_root}/etc/systemd/system" mkdir -p "${svc_dir}" cat > "${svc_dir}/chromium-hwdecode.service" << 'SVC' [Unit] Description=Chromium HW decode setup (DW mode + GPU freq) After=multi-user.target [Service] Type=oneshot ExecStart=/bin/true RemainAfterExit=yes [Install] WantedBy=multi-user.target SVC # Enable service via postinst mkdir -p "${pkg_root}/DEBIAN" cat > "${pkg_root}/DEBIAN/postinst" << 'POSTINST' #!/bin/sh systemctl daemon-reload systemctl enable chromium-hwdecode.service systemctl start chromium-hwdecode.service 2>/dev/null || true POSTINST chmod 755 "${pkg_root}/DEBIAN/postinst" # DEBIAN control cat > "${pkg_root}/DEBIAN/control" << EOF Package: ${DEB_NAME} Version: ${DEB_VERSION} Architecture: arm64 Maintainer: AMLbian Depends: libc6, libglib2.0-0, libnss3, libatk1.0-0, libatk-bridge2.0-0, libcups2, libdrm2, libgbm1, libpango-1.0-0, libcairo2, libasound2, libxkbcommon0, libwayland-client0, pipewire-pulse | pulseaudio, aml-libgles-eabihf-avalon-wayland-drm Description: Chromium ${CHROMIUM_VERSION} with AML HW video decode V4L2 hardware decoding with DMA-BUF zero-copy for Amlogic SoCs. Supports H.264, VP9, HEVC up to 4K@60fps. EOF # Build deb mkdir -p "${BUILD_DEBS}" local deb_file="${BUILD_DEBS}/${DEB_NAME}_${DEB_VERSION}_arm64.deb" # Written as .part and renamed when complete, so a cut-off dpkg-deb never leaves a deb to reuse if ! dpkg-deb --build --root-owner-group "${pkg_root}" "${deb_file}.part" || \ ! mv -f "${deb_file}.part" "${deb_file}"; then rm -rf "${pkg_root}"; display_alert "CHROMIUM" "ERROR: cannot build ${deb_file}" "err"; return 1 fi rm -rf "${pkg_root}" display_alert "CHROMIUM" "Package: ${deb_file}" "info" ls -lh "${deb_file}" } # ============================================================================== # Section 4: Main Build Function # ============================================================================== function amlbian_build_chromium() { __chromium_check_enabled || return 0 # A reused tree keeps the patches it was built with, so the tree records a checksum of patches/, # overlay/ and the code extracting and patching it, and is rebuilt from the tarball when that changes. # The deb records that checksum plus config/ and the build and packaging code, and the deb of this # version is reused only while all of it matches. Otherwise no deb of any version stays at the top of # BUILD_DEBS (lib/function-img.sh installs any). Code is hashed via declare -f: comments are ignored, # but a bash upgrade may change its output once. After a tree rebuild clang and the sysroot are linked # from ${CHROMIUM_BUILD_ROOT}/src when it has them, else fetched (see __chromium_configure). local sum key old deb_file="${BUILD_DEBS}/${DEB_NAME}_${DEB_VERSION}_arm64.deb" if ! sum=$(set -o pipefail; cd "${SCRIPT_DIR}" && { declare -f __chromium_fetch_source __chromium_apply_patches && find patches overlay -type f -print0 | LC_ALL=C sort -z | xargs -0 -r sha256sum; } | sha256sum) || ! key=$(set -o pipefail; cd "${SCRIPT_DIR}" && { echo "${sum}" && declare -f __chromium_configure __chromium_compile __chromium_package_deb && find config -type f -print0 | LC_ALL=C sort -z | xargs -0 -r sha256sum; } | sha256sum); then display_alert "CHROMIUM" "ERROR: cannot checksum the chromium build inputs" "err"; return 1 fi if [[ -s "${deb_file}" && "$(cat "${deb_file}.sum" 2>/dev/null || true)" == "${key}" ]]; then display_alert "CHROMIUM" "Pre-built deb found: ${deb_file##*/}, skipping build" "info" return 0 fi old=$(cat "${CHROMIUM_SRC}/.aml-patches.sum" 2>/dev/null || true) if [[ -d "${BUILD_DEBS}" ]]; then find -H "${BUILD_DEBS}" -maxdepth 1 \( -name "${DEB_NAME}_*.deb" -o -name "${DEB_NAME}_*.deb.*" \) \ -printf "Removing %p\n" -delete || \ { display_alert "CHROMIUM" "ERROR: cannot remove old debs in ${BUILD_DEBS}" "err"; return 1; } fi if [[ "${old}" != "${sum}" ]]; then local why="not recorded"; [[ -z "${old}" ]] || why="inputs changed" display_alert "CHROMIUM" "Tree ${why}: rebuilding ${CHROMIUM_SRC} from the tarball" "wrn" [[ "${CHROMIUM_SRC##*/}" == "chromium-${CHROMIUM_VERSION}" ]] || \ { display_alert "CHROMIUM" "ERROR: unexpected CHROMIUM_SRC ${CHROMIUM_SRC}" "err"; return 1; } # The record goes first, so an interrupted removal never leaves a recorded but partial tree if ! rm -f "${CHROMIUM_SRC}/.aml-patches.sum" || ! rm -rf "${CHROMIUM_SRC}"; then display_alert "CHROMIUM" "ERROR: cannot remove ${CHROMIUM_SRC}" "err"; return 1 fi fi display_alert "CHROMIUM" "No deb with matching inputs, compiling from source..." "info" # A step that changes the tree or the deb must be in one of the declare -f lists above __chromium_fetch_source || { display_alert "CHROMIUM" "ABORT: fetch failed" "err"; return 1; } __chromium_apply_patches || { display_alert "CHROMIUM" "ABORT: patch failed" "err"; return 1; } echo "${sum}" > "${CHROMIUM_SRC}/.aml-patches.sum" || \ { display_alert "CHROMIUM" "ERROR: cannot write ${CHROMIUM_SRC}/.aml-patches.sum" "err"; return 1; } __chromium_configure || { display_alert "CHROMIUM" "ABORT: configure failed" "err"; return 1; } __chromium_compile || { display_alert "CHROMIUM" "ABORT: compile failed" "err"; return 1; } __chromium_package_deb || { display_alert "CHROMIUM" "ABORT: package failed" "err"; return 1; } echo "${key}" > "${deb_file}.sum" || \ { display_alert "CHROMIUM" "ERROR: cannot write ${deb_file}.sum" "err"; return 1; } } # ============================================================================== # Section 5: Main # ============================================================================== display_alert "AMLbian Chromium Build Script" "Version: ${CHROMIUM_VERSION}" "info" # Run in subshell to isolate PATH/env changes from parent build system. # Other build scripts (kernel, uboot) run directly with set -e, but chromium # modifies PATH (depot_tools), PYTHONHOME, etc. which would affect subsequent # scripts if not isolated. (amlbian_build_chromium) # ============================================================================== # End of build-chromium.sh # ==============================================================================