#!/bin/bash
# Point the OpenWrt build at an installed Go toolchain.
#
# Go based packages (tailscale, adguardhome, easytier, cloudflared, ...) need a
# host Go. By default OpenWrt builds the whole bootstrap chain from source; an
# already installed Go can replace part or all of that:
#
# --external GOLANG_BUILD_BOOTSTRAP=n, host Go is built with the
# installed toolchain directly (skips the entire chain,
# requires a Go new enough for the target version)
# default keep GOLANG_BUILD_BOOTSTRAP=y and only set the root, which
# skips the initial bootstrap step
#
# Usage: ./update-go-path.sh [options]
#
# --external skip the bootstrap chain entirely (recommended)
# --go-root
use this Go instead of auto-detecting
# --skip-defconfig do not run make defconfig afterwards
# -n, --dry-run show what would change, write nothing
# -h, --help show this help
#
# Run it from the OpenWrt root: ./helper/update-go-path.sh
#
# Author: Zhe Yuan
set -euo pipefail
umask 022
CONFIG_FILE=".config"
EXTERNAL=false
GO_ROOT=""
RUN_DEFCONFIG=true
DRY_RUN=false
info() { echo "[INFO] $*"; }
warn() { echo "[WARN] $*"; }
err() { echo "[ERROR] $*" >&2; }
usage() { sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//;s/^#$//'; }
while [ $# -gt 0 ]; do
case "$1" in
--external) EXTERNAL=true ;;
--go-root)
if [ $# -lt 2 ] || [ -z "${2:-}" ]; then err "--go-root requires a directory."; exit 1; fi
GO_ROOT="$2"; shift ;;
--skip-defconfig) RUN_DEFCONFIG=false ;;
-n|--dry-run) DRY_RUN=true ;;
-h|--help) usage; exit 0 ;;
*) err "Unknown option: $1"; echo "Run '$0 --help' for usage." >&2; exit 1 ;;
esac
shift
done
echo "========================================"
echo " Go Toolchain Setup for OpenWrt"
echo "========================================"
if [ ! -f "$CONFIG_FILE" ]; then
err "No .config in the current directory."
err "From the OpenWrt root, run it as: ./helper/$(basename "$0")"
exit 1
fi
# --- Locate a Go installation --------------------------------------------
if [ -z "$GO_ROOT" ]; then
# nullglob only drops words that actually contain a wildcard; a literal path
# such as /usr/local/go/ survives even when it does not exist. So collect
# candidates and keep only the directories that are really there.
shopt -s nullglob
CANDIDATES=(/usr/lib/go-*/ /usr/local/go/ /usr/lib/golang/)
shopt -u nullglob
GO_DIRS=()
for dir in "${CANDIDATES[@]}"; do
[ -d "$dir" ] && GO_DIRS+=("$dir")
done
if [ ${#GO_DIRS[@]} -eq 0 ]; then
err "No Go installation found under /usr/lib/go-*, /usr/local/go or /usr/lib/golang."
err "Install one with: sudo apt install golang-1.24-go"
err "or keep the default, where OpenWrt builds its own bootstrap chain."
exit 1
fi
BEST_VER=""
for dir in "${GO_DIRS[@]}"; do
[ -x "${dir}bin/go" ] || continue
ver="$("${dir}bin/go" version 2>/dev/null | awk '{print $3}' | sed 's/^go//')"
[ -n "$ver" ] || continue
if [ -z "$BEST_VER" ] ||
[ "$(printf '%s\n%s\n' "$BEST_VER" "$ver" | sort -V | tail -n 1)" = "$ver" ]; then
BEST_VER="$ver"
GO_ROOT="$dir"
fi
done
if [ -z "$GO_ROOT" ]; then
err "Found Go directories but none contained a working bin/go."
exit 1
fi
if [ ${#GO_DIRS[@]} -gt 1 ]; then
info "Go installations found: ${GO_DIRS[*]}"
info "Using the newest; override with --go-root ."
fi
fi
GO_ROOT="${GO_ROOT%/}/"
if [ ! -x "${GO_ROOT}bin/go" ]; then
err "No usable Go compiler at ${GO_ROOT}bin/go"
exit 1
fi
GO_VER="$("${GO_ROOT}bin/go" version | awk '{print $3}' | sed 's/^go//')"
info "Go toolchain: $GO_ROOT (version $GO_VER)"
# --- Warn if it is older than what the tree wants -------------------------
BOOTSTRAP_MK="feeds/packages/lang/golang/golang-bootstrap/Makefile"
if [ -f "$BOOTSTRAP_MK" ]; then
NEED="$(awk -F':=' '/^GO_VERSION_MAJOR_MINOR/ {print $2; exit}' "$BOOTSTRAP_MK" | tr -d ' ')"
if [ -n "$NEED" ] &&
[ "$(printf '%s\n%s\n' "$NEED" "$GO_VER" | sort -V | head -n 1)" != "$NEED" ]; then
warn "Installed Go $GO_VER is older than the bootstrap version $NEED this tree expects."
warn "The build may fail; consider installing golang-$NEED-go."
fi
fi
# --- Apply ----------------------------------------------------------------
set_config() {
local key="$1" val="$2"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] $key=$val"
return
fi
# Drop every existing form of the symbol, then append once.
sed -i "/^${key}=/d;/^# ${key} is not set$/d" "$CONFIG_FILE"
if [ "$val" = "n" ]; then
echo "# $key is not set" >> "$CONFIG_FILE"
else
echo "$key=$val" >> "$CONFIG_FILE"
fi
}
set_config CONFIG_GOLANG_EXTERNAL_BOOTSTRAP_ROOT "\"$GO_ROOT\""
if [ "$EXTERNAL" = true ]; then
set_config CONFIG_GOLANG_BUILD_BOOTSTRAP n
info "Bootstrap chain disabled; host Go is built with the installed toolchain."
else
set_config CONFIG_GOLANG_BUILD_BOOTSTRAP y
info "Bootstrap chain kept (only the initial step is skipped)."
info "Pass --external to skip it entirely."
fi
if [ "$DRY_RUN" = true ]; then
echo
info "Dry run complete; .config was not modified."
exit 0
fi
if [ "$RUN_DEFCONFIG" = true ]; then
info "Running make defconfig..."
make defconfig >/dev/null
fi
echo
echo "========================================"
echo "OK Go configuration updated."
echo "========================================"
grep -E '^(CONFIG_GOLANG_EXTERNAL_BOOTSTRAP_ROOT|CONFIG_GOLANG_BUILD_BOOTSTRAP)|^# CONFIG_GOLANG_BUILD_BOOTSTRAP' "$CONFIG_FILE" | sed 's/^/ /'
echo