# Extension: ccache-remote # Enables ccache with remote storage for sharing compilation cache across build hosts. # Supports Redis and HTTP/WebDAV backends (ccache 4.4+). # # Documentation: # Redis: https://ccache.dev/howto/redis-storage.html # HTTP: https://ccache.dev/howto/http-storage.html # General: https://ccache.dev/manual/4.10.html#config_remote_storage # # Usage: # # With explicit Redis server: # ./compile.sh ENABLE_EXTENSIONS=ccache-remote CCACHE_REMOTE_STORAGE="redis://192.168.1.65:6379" BOARD=... # # # With HTTP/WebDAV server: # ./compile.sh ENABLE_EXTENSIONS=ccache-remote CCACHE_REMOTE_STORAGE="http://192.168.1.65:8088/ccache/" BOARD=... # # # Auto-discovery via DNS-SD (no URL needed, discovers type/host/port): # ./compile.sh ENABLE_EXTENSIONS=ccache-remote BOARD=... # # # DNS SRV discovery for remote build servers: # ./compile.sh ENABLE_EXTENSIONS=ccache-remote CCACHE_REMOTE_DOMAIN="example.com" BOARD=... # # # Disable local cache, use remote only (saves local disk space): # ./compile.sh ENABLE_EXTENSIONS=ccache-remote CCACHE_REMOTE_ONLY=yes BOARD=... # # Automatically sets USE_CCACHE=yes # # Supported ccache environment variables (passed through to builds): # See: https://ccache.dev/manual/latest.html#_configuration_options # CCACHE_BASEDIR - base directory for path normalization (enables cache sharing) # CCACHE_REMOTE_STORAGE - remote storage URL (redis://... or http://...) # CCACHE_REMOTE_DOMAIN - domain for DNS SRV discovery (e.g., "example.com") # CCACHE_REMOTE_ONLY - use only remote storage, disable local cache # CCACHE_READONLY - read-only mode, don't update cache # CCACHE_RECACHE - don't use cached results, but update cache # CCACHE_RESHARE - rewrite cache entries to remote storage # CCACHE_DISABLE - disable ccache completely # CCACHE_MAXSIZE - maximum cache size (e.g., "10G") # CCACHE_MAXFILES - maximum number of files in cache # CCACHE_NAMESPACE - cache namespace for isolation # CCACHE_SLOPPINESS - comma-separated list of sloppiness options # CCACHE_UMASK - umask for cache files # CCACHE_LOGFILE - path to log file # CCACHE_DEBUGLEVEL - debug level (1-2) # CCACHE_STATSLOG - path to stats log file # CCACHE_PCH_EXTSUM - include PCH extension in hash # # CCACHE_REMOTE_STORAGE format (ccache 4.4+): # Redis: redis://[[USERNAME:]PASSWORD@]HOST[:PORT][|attribute=value...] # HTTP: http://HOST[:PORT]/PATH/[|attribute=value...] # Common attributes: # connect-timeout=N - connection timeout in milliseconds (default: 100) # operation-timeout=N - operation timeout in milliseconds (default: 10000) # Examples: # "redis://default:secretpass@192.168.1.65:6379|connect-timeout=500" # "redis://192.168.1.65:6379|connect-timeout=500" # "http://192.168.1.65:8088/ccache/" # # Auto-discovery (priority order): # 1. Explicit CCACHE_REMOTE_STORAGE - used as-is, no discovery # 2. DNS-SD browse for _ccache._tcp on local network (avahi-browse) # 3. DNS SRV record _ccache._tcp.DOMAIN (when CCACHE_REMOTE_DOMAIN is set) # 4. Legacy mDNS: resolve 'ccache.local' hostname (fallback) # # When multiple services are found, Redis is preferred over HTTP. # # DNS-SD service publication (on cache server): # # For HTTP/WebDAV: # avahi-publish-service "ccache-webdav" _ccache._tcp 8088 type=http path=/ccache/ # # For Redis: # avahi-publish-service "ccache-redis" _ccache._tcp 6379 type=redis # # DNS SRV record (for remote/hosted build servers): # Set CCACHE_REMOTE_DOMAIN to your domain, then create DNS records: # _ccache._tcp.example.com. SRV 0 0 8088 ccache.example.com. # _ccache._tcp.example.com. TXT "type=http" "path=/ccache/" # The cache server must be reachable from the build host (e.g., via port forwarding). # # Legacy mDNS (backward compatible): # Publish 'ccache.local' hostname via Avahi: # avahi-publish-address -R ccache.local # Or create a systemd service (see below). # # Server setup: see README.server-setup.md and config files in misc/ # - misc/redis/redis-ccache.conf — Redis configuration example # - misc/nginx/ccache-webdav.conf — nginx WebDAV configuration example # - misc/avahi/ccache-*.service — Avahi DNS-SD service files (static, always announce) # - misc/systemd/ccache-avahi-*.service — systemd units (announce only while service runs) # # Fallback behavior: # If CCACHE_REMOTE_STORAGE is not set and ccache.local is not resolvable, # extension silently falls back to local ccache only. # # Cache sharing requirements: # For cache to be shared across multiple build hosts, the Armbian project # path must be identical on all machines (e.g., /home/build/armbian). # This is because ccache includes the working directory in the cache key. # Docker builds automatically use consistent paths (/armbian/...). # Default Redis connection timeout in milliseconds (can be overridden by user) # Note: Must be set before extension loads (e.g., via environment or command line) declare -g -r CCACHE_REDIS_CONNECT_TIMEOUT="${CCACHE_REDIS_CONNECT_TIMEOUT:-500}" # List of ccache environment variables to pass through to builds declare -g -a CCACHE_PASSTHROUGH_VARS=( CCACHE_REDIS_CONNECT_TIMEOUT CCACHE_REMOTE_DOMAIN CCACHE_BASEDIR CCACHE_REMOTE_STORAGE CCACHE_REMOTE_ONLY CCACHE_READONLY CCACHE_RECACHE CCACHE_RESHARE CCACHE_DISABLE CCACHE_MAXSIZE CCACHE_MAXFILES CCACHE_NAMESPACE CCACHE_SLOPPINESS CCACHE_UMASK CCACHE_LOGFILE CCACHE_DEBUGLEVEL CCACHE_STATSLOG CCACHE_PCH_EXTSUM ) # Format host:port, wrapping IPv6 addresses in brackets for URL compatibility (RFC 2732) function ccache_format_host_port() { local host="$1" port="$2" if [[ "${host}" == *:* ]]; then echo "[${host}]:${port}" else echo "${host}:${port}" fi } # Extract hostname from CCACHE_REMOTE_STORAGE URL (strips scheme, userinfo, port, path) function ccache_extract_url_host() { local url="$1" local after_scheme="${url#*://}" # Strip userinfo if present if [[ "${after_scheme}" == *@* ]]; then after_scheme="${after_scheme##*@}" fi local host # Handle bracketed IPv6: [addr]:port if [[ "${after_scheme}" == \[* ]]; then host="${after_scheme#\[}" host="${host%%\]*}" else # Strip port, path, and ccache attributes host="${after_scheme%%[:\/|]*}" fi echo "${host}" } # Discover ccache remote storage via DNS-SD (mDNS/Avahi) or DNS SRV records. # Looks for _ccache._tcp services with TXT records: type=http|redis, path=/... # Prefers Redis over HTTP when multiple services are found. # Sets CCACHE_REMOTE_STORAGE on success, returns 1 if nothing found. function ccache_discover_remote_storage() { # Method 1: DNS-SD browse on local network (requires avahi-browse) if command -v avahi-browse &>/dev/null; then local browse_output browse_output=$(timeout 5 avahi-browse -rpt _ccache._tcp 2>/dev/null || true) if [[ -n "${browse_output}" ]]; then # Parse resolved lines: =;IFACE;PROTO;NAME;TYPE;DOMAIN;HOSTNAME;ADDRESS;PORT;"txt"... # Prefer IPv4 (proto=IPv4), prefer type=redis over type=http local redis_url="" http_url="" local redis_host="" redis_host_ip="" http_host="" http_host_ip="" while IFS=';' read -r status iface proto name stype domain hostname address port txt_rest; do [[ "${status}" == "=" && "${proto}" == "IPv4" ]] || continue local svc_type="" svc_path="" # Parse TXT records from remaining fields if [[ "${txt_rest}" =~ \"type=([a-z]+)\" ]]; then svc_type="${BASH_REMATCH[1]}" fi if [[ "${txt_rest}" =~ \"path=([^\"]+)\" ]]; then svc_path="${BASH_REMATCH[1]}" fi # Use hostname for URL (Docker --add-host resolves it), fall back to address local svc_host="${hostname%.local}" svc_host="${svc_host%.}" [[ -z "${svc_host}" ]] && svc_host="${address}" if [[ "${svc_type}" == "redis" ]]; then redis_url="redis://${svc_host}:${port}|connect-timeout=${CCACHE_REDIS_CONNECT_TIMEOUT}" redis_host="${svc_host}" redis_host_ip="${address}" elif [[ "${svc_type}" == "http" ]]; then http_url="http://${svc_host}:${port}${svc_path}" http_host="${svc_host}" http_host_ip="${address}" fi done <<< "${browse_output}" # Redis preferred over HTTP; set hostname->IP mapping for Docker --add-host if [[ -n "${redis_url}" ]]; then export CCACHE_REMOTE_STORAGE="${redis_url}" declare -g CCACHE_REMOTE_HOST="${redis_host}" declare -g CCACHE_REMOTE_HOST_IP="${redis_host_ip}" display_alert "DNS-SD: discovered Redis ccache" "$(ccache_mask_storage_url "${CCACHE_REMOTE_STORAGE}")" "info" return 0 elif [[ -n "${http_url}" ]]; then export CCACHE_REMOTE_STORAGE="${http_url}" declare -g CCACHE_REMOTE_HOST="${http_host}" declare -g CCACHE_REMOTE_HOST_IP="${http_host_ip}" display_alert "DNS-SD: discovered HTTP ccache" "$(ccache_mask_storage_url "${CCACHE_REMOTE_STORAGE}")" "info" return 0 fi fi fi # Method 2: DNS SRV record for remote setups (CCACHE_REMOTE_DOMAIN must be set) if [[ -n "${CCACHE_REMOTE_DOMAIN}" ]] && command -v dig &>/dev/null; then local srv_output srv_output=$(dig +short SRV "_ccache._tcp.${CCACHE_REMOTE_DOMAIN}" 2>/dev/null || true) if [[ -n "${srv_output}" ]]; then local srv_port srv_host # SRV format: priority weight port target read -r _ _ srv_port srv_host <<< "${srv_output}" srv_host="${srv_host%.}" # strip trailing dot if [[ -n "${srv_host}" && -n "${srv_port}" ]]; then # Check TXT record for service type and path local txt_output svc_type="redis" svc_path="" txt_output=$(dig +short TXT "_ccache._tcp.${CCACHE_REMOTE_DOMAIN}" 2>/dev/null || true) if [[ "${txt_output}" =~ type=([a-z]+) ]]; then svc_type="${BASH_REMATCH[1]}" fi if [[ "${txt_output}" =~ path=([^\"[:space:]]+) ]]; then svc_path="${BASH_REMATCH[1]}" fi local host_port host_port=$(ccache_format_host_port "${srv_host}" "${srv_port}") if [[ "${svc_type}" == "http" ]]; then export CCACHE_REMOTE_STORAGE="http://${host_port}${svc_path}" else export CCACHE_REMOTE_STORAGE="redis://${host_port}|connect-timeout=${CCACHE_REDIS_CONNECT_TIMEOUT}" fi display_alert "DNS SRV: discovered ccache" "$(ccache_mask_storage_url "${CCACHE_REMOTE_STORAGE}")" "info" return 0 fi fi fi # Method 3: Legacy fallback - resolve ccache.local hostname local ccache_ip ccache_ip=$(getent hosts ccache.local 2>/dev/null | awk '{print $1; exit}' || true) if [[ -n "${ccache_ip}" ]]; then local host_port host_port=$(ccache_format_host_port "${ccache_ip}" "6379") export CCACHE_REMOTE_STORAGE="redis://${host_port}|connect-timeout=${CCACHE_REDIS_CONNECT_TIMEOUT}" display_alert "mDNS: discovered ccache" "$(ccache_mask_storage_url "${CCACHE_REMOTE_STORAGE}")" "info" return 0 fi return 1 } # Query Redis stats (keys count and memory usage) function ccache_get_redis_stats() { local ip="$1" local port="${2:-6379}" local password="$3" local stats="" if command -v redis-cli &>/dev/null; then local auth_args=() [[ -n "${password}" ]] && auth_args+=(-a "${password}" --no-auth-warning) local keys mem keys=$(timeout 2 redis-cli -h "$ip" -p "$port" "${auth_args[@]}" DBSIZE 2>/dev/null | grep -oE '[0-9]+' || true) mem=$(timeout 2 redis-cli -h "$ip" -p "$port" "${auth_args[@]}" INFO memory 2>/dev/null | grep "used_memory_human" | cut -d: -f2 | tr -d '[:space:]' || true) if [[ -n "$keys" ]]; then stats="keys=${keys:-0}, mem=${mem:-?}" fi else # Fallback: try netcat for basic connectivity check if nc -z -w 2 "$ip" "$port" 2>/dev/null; then stats="reachable (redis-cli not installed for detailed stats)" fi fi echo "$stats" } # Check HTTP/WebDAV storage reachability via HEAD request function ccache_get_http_stats() { local url="$1" local stats="" local http_code http_code=$(timeout 3 curl -s -o /dev/null -w "%{http_code}" -X HEAD "${url}" 2>/dev/null || true) if [[ -n "${http_code}" && "${http_code}" != "000" ]]; then stats="reachable (HTTP ${http_code})" fi echo "$stats" } # Query remote storage stats based on URL scheme (redis:// or http://) # Parses userinfo (user:pass@) from Redis URLs to pass credentials to redis-cli function ccache_get_remote_stats() { local url="$1" if [[ "${url}" =~ ^redis:// ]]; then local password="" host="" port="6379" # Strip scheme and attributes local authority="${url#redis://}" authority="${authority%%|*}" # Extract password from userinfo (before last @) if [[ "${authority}" =~ ^(.+)@(.+)$ ]]; then local userinfo="${BASH_REMATCH[1]}" authority="${BASH_REMATCH[2]}" # password is after : in userinfo (user:pass or just :pass) [[ "${userinfo}" == *:* ]] && password="${userinfo#*:}" fi # Parse host:port (IPv6 in brackets or plain) if [[ "${authority}" =~ ^\[([^]]+)\]:?([0-9]*) ]]; then host="${BASH_REMATCH[1]}" [[ -n "${BASH_REMATCH[2]}" ]] && port="${BASH_REMATCH[2]}" elif [[ "${authority}" =~ ^([^:]+):?([0-9]*) ]]; then host="${BASH_REMATCH[1]}" [[ -n "${BASH_REMATCH[2]}" ]] && port="${BASH_REMATCH[2]}" fi [[ -n "${host}" ]] && ccache_get_redis_stats "${host}" "${port}" "${password}" elif [[ "${url}" =~ ^https?:// ]]; then # Strip ccache attributes after | for the URL ccache_get_http_stats "${url%%|*}" fi } # Mask credentials in storage URLs to avoid leaking secrets into build logs # Handles any URI scheme with userinfo component (e.g., redis://user:pass@host) # Uses last @ as delimiter since userinfo may contain special characters function ccache_mask_storage_url() { local url="$1" if [[ "${url}" =~ ^([a-zA-Z][a-zA-Z0-9+.-]*://)(.+)@([^@]+)$ ]]; then echo "${BASH_REMATCH[1]}****@${BASH_REMATCH[3]}" else echo "${url}" fi } # Validate that credentials in storage URL do not contain characters unsafe for URL parsing. # Passwords with / + = or spaces break URL parsing in ccache and in our mask function. # Returns 1 and displays error if invalid characters are found. function ccache_validate_storage_url() { local url="$1" # Extract userinfo (part between :// and last @) if [[ "${url}" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*://(.+)@[^@]+$ ]]; then local userinfo="${BASH_REMATCH[1]}" if [[ "${userinfo}" =~ [/+=[:space:]] ]]; then display_alert "Password contains URL-unsafe characters (/ + = or spaces)" \ "Generate a safe password: openssl rand -hex 24" "err" return 1 fi fi return 0 } # This runs on the HOST just before Docker container is launched. # Resolves 'ccache.local' via mDNS (requires Avahi on server publishing this hostname # Docker hook: resolve hostnames and handle loopback for container access. # mDNS/local DNS may not work inside Docker, so we resolve on host and # pass the mapping via --add-host. Loopback addresses are rewritten to # host.docker.internal. function host_pre_docker_launch__setup_remote_ccache() { if [[ -n "${CCACHE_REMOTE_STORAGE}" ]]; then ccache_validate_storage_url "${CCACHE_REMOTE_STORAGE}" || return 1 display_alert "Remote ccache pre-configured" "$(ccache_mask_storage_url "${CCACHE_REMOTE_STORAGE}")" "info" elif ! ccache_discover_remote_storage; then display_alert "Remote ccache not found on host" "no service discovered" "debug" fi # Show backend stats if we have a remote storage URL if [[ -n "${CCACHE_REMOTE_STORAGE}" ]]; then local stats stats=$(ccache_get_remote_stats "${CCACHE_REMOTE_STORAGE}") if [[ -n "$stats" ]]; then display_alert "Remote ccache stats" "${stats}" "info" fi fi # Ensure hostname in CCACHE_REMOTE_STORAGE is resolvable inside Docker. # Docker containers may not have access to host mDNS/local DNS. if [[ -n "${CCACHE_REMOTE_STORAGE}" ]]; then local _host _host=$(ccache_extract_url_host "${CCACHE_REMOTE_STORAGE}") if [[ -n "${_host}" ]]; then # Loopback addresses: rewrite to host.docker.internal if [[ "${_host}" == "localhost" || "${_host}" == "127.0.0.1" || "${_host}" == "::1" ]]; then CCACHE_REMOTE_STORAGE="${CCACHE_REMOTE_STORAGE//localhost/host.docker.internal}" CCACHE_REMOTE_STORAGE="${CCACHE_REMOTE_STORAGE//127.0.0.1/host.docker.internal}" CCACHE_REMOTE_STORAGE="${CCACHE_REMOTE_STORAGE//\[::1\]/host.docker.internal}" DOCKER_EXTRA_ARGS+=("--add-host=host.docker.internal:host-gateway") display_alert "Rewriting loopback URL for Docker" "$(ccache_mask_storage_url "${CCACHE_REMOTE_STORAGE}")" "info" # Hostname (not IP): resolve on host and pass via --add-host elif [[ "${_host}" =~ [a-zA-Z] ]]; then local _resolved_ip="${CCACHE_REMOTE_HOST_IP:-}" # If not from discovery, resolve now; prefer IPv4 (Docker bridge often lacks IPv6) if [[ -z "${_resolved_ip}" || "${CCACHE_REMOTE_HOST}" != "${_host}" ]]; then _resolved_ip=$(getent ahostsv4 "${_host}" 2>/dev/null | awk '{print $1; exit}' || true) [[ -z "${_resolved_ip}" ]] && _resolved_ip=$(getent hosts "${_host}" 2>/dev/null | awk '{print $1; exit}' || true) fi if [[ -n "${_resolved_ip}" ]]; then DOCKER_EXTRA_ARGS+=("--add-host=${_host}:${_resolved_ip}") display_alert "Docker --add-host" "${_host}:${_resolved_ip}" "info" else display_alert "Cannot resolve hostname for Docker" "${_host}" "wrn" fi fi fi fi # Pass all set CCACHE_* variables to Docker local var val for var in "${CCACHE_PASSTHROUGH_VARS[@]}"; do val="${!var}" if [[ -n "${val}" ]]; then DOCKER_EXTRA_ARGS+=("--env" "${var}=${val}") local log_val="${val}" [[ "${var}" == "CCACHE_REMOTE_STORAGE" ]] && log_val="$(ccache_mask_storage_url "${val}")" display_alert "Docker env" "${var}=${log_val}" "debug" fi done } # Hook: Show ccache remote storage statistics after each compilation (kernel, uboot) function ccache_post_compilation__show_remote_stats() { if [[ -n "${CCACHE_REMOTE_STORAGE}" ]]; then local stats_output pct local read_hit read_miss write error stats_output=$(ccache --print-stats 2>&1 || true) read_hit=$(ccache_get_stat "$stats_output" "remote_storage_read_hit") read_miss=$(ccache_get_stat "$stats_output" "remote_storage_read_miss") write=$(ccache_get_stat "$stats_output" "remote_storage_write") error=$(ccache_get_stat "$stats_output" "remote_storage_error") pct=$(ccache_hit_pct "$read_hit" "$read_miss") display_alert "Remote ccache result" "hit=${read_hit} miss=${read_miss} write=${write} err=${error} (${pct}%)" "info" fi } # This runs inside Docker (or native build) during configuration function extension_prepare_config__setup_remote_ccache() { # Enable ccache with a consistent cache directory ($SRC/cache/ccache). # PRIVATE_CCACHE ensures the same CCACHE_DIR is used in native and Docker builds, # avoiding fragmented caches in /root/.cache/ccache vs $SRC/cache/ccache. declare -g USE_CCACHE=yes declare -g PRIVATE_CCACHE=yes # If CCACHE_REMOTE_STORAGE was passed from host (via Docker env), it's already set if [[ -n "${CCACHE_REMOTE_STORAGE}" ]]; then ccache_validate_storage_url "${CCACHE_REMOTE_STORAGE}" || return 1 display_alert "Remote ccache configured" "$(ccache_mask_storage_url "${CCACHE_REMOTE_STORAGE}")" "info" return 0 fi # For native (non-Docker) builds, try to discover if ccache_discover_remote_storage; then return 0 fi if [[ "${CCACHE_REMOTE_ONLY}" == "yes" ]]; then display_alert "Remote ccache not available" "CCACHE_REMOTE_ONLY=yes but no remote found, ccache will be ineffective" "wrn" else display_alert "Remote ccache not available" "using local cache only" "debug" fi return 0 } # Inject all set CCACHE_PASSTHROUGH_VARS into the given make environment array # Uses bash nameref to write into the caller's array variable function ccache_inject_envs() { local -n target_array="$1" local label="$2" local var val for var in "${CCACHE_PASSTHROUGH_VARS[@]}"; do val="${!var}" if [[ -n "${val}" ]]; then target_array+=("${var}=${val@Q}") local log_val="${val}" [[ "${var}" == "CCACHE_REMOTE_STORAGE" ]] && log_val="$(ccache_mask_storage_url "${val}")" display_alert "${label}: ${var}" "${log_val}" "debug" fi done } # This hook runs right before kernel make - add ccache env vars to make environment. # Required because kernel build uses 'env -i' which clears all environment variables. function kernel_make_config__add_ccache_remote_storage() { ccache_inject_envs common_make_envs "Kernel make" } # This hook runs right before u-boot make - add ccache env vars to make environment. # Required because u-boot build uses 'env -i' which clears all environment variables. function uboot_make_config__add_ccache_remote_storage() { ccache_inject_envs uboot_make_envs "U-boot make" }