The ##built-in section of each package list was copied by hand from the Firmware Selector for every new release. It is now generated from the target's profiles.json (default_packages + device_packages, plus the two packages the Selector adds on top), which reproduces the hand-written list byte for byte, order included. New lib-openwrt-upstream.sh holds the shared upstream access: URL construction, cached fetches, version detection, device search over .overview.json and profiles.json parsing. It needs python3, not jq. New gen-package-list.sh writes config_<ver>/packages_<dev>_<ver>.txt, replacing only the generated block so hand-written extras and the ##module section survive; --stdout and --apply skip the file entirely. download-config.sh: - Look the device up in the official index instead of a hardcoded table of three devices, so any supported device can be selected by name. - Build the firmware URLs from profiles.json images[]. The old code pasted the target into the filename with its '/' intact, so the URL was always wrong and the check always warned. - Stage the download in a temp file and validate it before touching .config, and keep a backup. A 404 used to truncate .config to nothing. - Fix version detection: `git describe --tags --abbrev=0` returns a tag whenever any tag is reachable, so the entire fallback chain below it was unreachable and a branch tip silently produced the wrong release URL. - Build only the selected device instead of all 35 in the target, and turn off the buildbot flags (ALL_KMODS, ALL_NONSHARED, SDK, IB, MAKE_TOOLCHAIN, COLLECT_KERNEL_DEBUG, AUTOREMOVE). Verified not to change the firmware: the set of packages built into the image is identical either way (193 packages), while the module packages to build drop from 1248 to 0. --keep-buildbot-flags and --all-profiles restore the old behaviour. - Assert the device symbol survived make defconfig, so a tree that does not match the release fails loudly instead of silently building the default. add-openwrt-packages.sh: - Rewrite existing CONFIG_PACKAGE_ lines in place. Repeated runs used to append duplicates and grow .config every time. - Support removal via a -pkg prefix or a ##remove section. - Fix the parser dropping a lone '#' or an unknown ##header into the package list, where it was reported as a missing package named '#'. - Read .config once into a map instead of grepping it per package, and compare package names exactly rather than as regexes. - After make defconfig, report any package whose final state differs from what was asked for, which is the only way to see a removal that a dependency pulled back in. Delete add-openwrt-packages_old.sh; nothing referenced it and it was a strict subset of the current script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
341 lines
9.7 KiB
Bash
Executable File
341 lines
9.7 KiB
Bash
Executable File
#!/bin/bash
|
|
# Apply a package list to the OpenWrt .config.
|
|
#
|
|
# The list is a plain text file of package names, whitespace separated, any
|
|
# number per line. Section headers switch how the following packages are
|
|
# applied, and a leading '-' on a name removes that package:
|
|
#
|
|
# ##built-in the packages after this are set to =y (default)
|
|
# ##module ... are set to =m
|
|
# ##remove ... are set to =n
|
|
# -luci-app-ttyd set to =n regardless of the current section
|
|
# pkg # comment text after a single # is ignored
|
|
#
|
|
# Existing CONFIG_PACKAGE_ lines are rewritten in place rather than appended,
|
|
# so running this repeatedly does not grow .config.
|
|
#
|
|
# Usage: ./add-openwrt-packages.sh [options] <package_list.txt | ->
|
|
#
|
|
# -y, --yes do not ask for confirmation
|
|
# -n, --dry-run show the resulting diff, change nothing
|
|
# --no-defconfig do not run make defconfig afterwards
|
|
# --no-backup do not keep a .config.bak
|
|
# --strict exit non-zero if any listed package was not found
|
|
# -h, --help show this help
|
|
#
|
|
# Run it from the OpenWrt root: ./helper/add-openwrt-packages.sh <list>
|
|
#
|
|
# Author: Zhe Yuan
|
|
|
|
set -euo pipefail
|
|
umask 022
|
|
|
|
PKG_FILE=""
|
|
ASSUME_YES=false
|
|
DRY_RUN=false
|
|
RUN_DEFCONFIG=true
|
|
KEEP_BACKUP=true
|
|
STRICT=false
|
|
|
|
info() { echo "[INFO] $*"; }
|
|
warn() { echo "[WARN] $*"; }
|
|
err() { echo "[ERROR] $*" >&2; }
|
|
|
|
usage() { sed -n '2,29p' "$0" | sed 's/^# \{0,1\}//;s/^#$//'; }
|
|
|
|
confirm() {
|
|
local question="$1" default="$2" reply=""
|
|
[ "$ASSUME_YES" = true ] && return 0
|
|
if [ "$default" = "yes" ]; then
|
|
read -r -p "$question [Y/n]: " reply || reply=""
|
|
case "${reply:-}" in [nN] | [nN][oO]) return 1 ;; *) return 0 ;; esac
|
|
else
|
|
read -r -p "$question [y/N]: " reply || reply=""
|
|
case "${reply:-}" in [yY] | [yY][eE][sS]) return 0 ;; *) return 1 ;; esac
|
|
fi
|
|
}
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
-y|--yes) ASSUME_YES=true ;;
|
|
-n|--dry-run) DRY_RUN=true ;;
|
|
--no-defconfig) RUN_DEFCONFIG=false ;;
|
|
--no-backup) KEEP_BACKUP=false ;;
|
|
--strict) STRICT=true ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
-) PKG_FILE="-" ;;
|
|
-*) err "Unknown option: $1"; echo "Run '$0 --help' for usage." >&2; exit 1 ;;
|
|
*)
|
|
if [ -n "$PKG_FILE" ]; then
|
|
err "Only one package list can be given."
|
|
exit 1
|
|
fi
|
|
PKG_FILE="$1"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[ -t 0 ] || ASSUME_YES=true
|
|
|
|
if [ -z "$PKG_FILE" ]; then
|
|
err "No package list given."
|
|
usage >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$PKG_FILE" != "-" ] && [ ! -f "$PKG_FILE" ]; then
|
|
err "Package list not found: $PKG_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -d package ] || [ ! -f Makefile ] || [ ! -f feeds.conf.default ]; then
|
|
err "Must run inside the OpenWrt root directory."
|
|
err "From the OpenWrt root, run it as: ./helper/$(basename "$0")"
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -f .config ]; then
|
|
info "No .config yet; running make defconfig first..."
|
|
make defconfig >/dev/null
|
|
fi
|
|
|
|
PACKAGE_INDEX="tmp/.config-package.in"
|
|
PACKAGE_INFO="tmp/.packageinfo"
|
|
if [ ! -f "$PACKAGE_INDEX" ] || [ ! -f "$PACKAGE_INFO" ]; then
|
|
info "Generating package metadata..."
|
|
make defconfig >/dev/null
|
|
fi
|
|
|
|
# --- Parse the list ------------------------------------------------------
|
|
# Section headers are handled BEFORE stripping comments: the old
|
|
# `sub(/#[^#].*/,"")` left a bare '#' behind, which was then reported as a
|
|
# missing package named '#'.
|
|
parse_list() {
|
|
awk '
|
|
BEGIN { mode = "y" }
|
|
{
|
|
line = $0
|
|
if (line ~ /^[ \t]*##/) {
|
|
if (line ~ /^[ \t]*##built-in/) { mode = "y"; next }
|
|
if (line ~ /^[ \t]*##module/) { mode = "m"; next }
|
|
if (line ~ /^[ \t]*##remove/) { mode = "n"; next }
|
|
print "[WARN] unknown section header, ignored: " line > "/dev/stderr"
|
|
next
|
|
}
|
|
sub(/#.*$/, "", line)
|
|
n = split(line, t, /[ \t]+/)
|
|
for (i = 1; i <= n; i++) {
|
|
tok = t[i]; m = mode
|
|
if (tok == "") continue
|
|
if (substr(tok, 1, 1) == "-") { tok = substr(tok, 2); m = "n" }
|
|
if (tok == "") continue
|
|
print tok "\t" m
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
if [ "$PKG_FILE" = "-" ]; then
|
|
ENTRIES="$(parse_list)"
|
|
PKG_LABEL="(stdin)"
|
|
else
|
|
ENTRIES="$(parse_list < "$PKG_FILE")"
|
|
PKG_LABEL="$PKG_FILE"
|
|
fi
|
|
|
|
if [ -z "$ENTRIES" ]; then
|
|
err "No packages found in $PKG_LABEL."
|
|
exit 1
|
|
fi
|
|
|
|
# --- Read the current state once -----------------------------------------
|
|
declare -A CUR=()
|
|
declare -A DEF=()
|
|
while IFS= read -r line; do
|
|
case "$line" in
|
|
CONFIG_PACKAGE_*=*) CUR["${line%%=*}"]="${line#*=}" ;;
|
|
"# CONFIG_PACKAGE_"*" is not set")
|
|
s="${line#\# }"
|
|
CUR["${s% is not set}"]="n"
|
|
;;
|
|
CONFIG_DEFAULT_*=y) DEF["${line%%=*}"]="y" ;;
|
|
esac
|
|
done < .config
|
|
|
|
# --- Verify and classify --------------------------------------------------
|
|
package_exists() {
|
|
local pkg="$1"
|
|
if [ -f "$PACKAGE_INDEX" ] &&
|
|
awk -v key="PACKAGE_$pkg" '$1 == "config" && $2 == key { found = 1; exit }
|
|
END { exit !found }' "$PACKAGE_INDEX"; then
|
|
return 0
|
|
fi
|
|
if [ -f "$PACKAGE_INFO" ] && grep -Fqx "Package: $pkg" "$PACKAGE_INFO"; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
DESIRED="$(mktemp "${TMPDIR:-/tmp}/desired.XXXXXX")"
|
|
NEWCFG="$(mktemp ./.config.new.XXXXXX)"
|
|
trap 'rm -f "$DESIRED" "$NEWCFG"' EXIT
|
|
|
|
declare -a MISSING=() SKIPPED=() PLANNED=() INVALID=()
|
|
|
|
while IFS=$'\t' read -r pkg mode; do
|
|
[ -z "$pkg" ] && continue
|
|
|
|
if ! [[ "$pkg" =~ ^[A-Za-z0-9._+-]+$ ]]; then
|
|
INVALID+=("$pkg")
|
|
continue
|
|
fi
|
|
|
|
sym="CONFIG_PACKAGE_$pkg"
|
|
cur="${CUR[$sym]:-}"
|
|
|
|
# Already provided by the target defaults: nothing to write, no churn.
|
|
if [ "$mode" = "y" ] && [ -n "${DEF[CONFIG_DEFAULT_$pkg]:-}" ] && [ "$cur" != "m" ]; then
|
|
SKIPPED+=("$pkg (target default)")
|
|
continue
|
|
fi
|
|
|
|
if [ "$cur" = "$mode" ]; then
|
|
SKIPPED+=("$pkg (already =$mode)")
|
|
continue
|
|
fi
|
|
|
|
# A removal of something that was never enabled needs no check.
|
|
if [ "$mode" != "n" ] && ! package_exists "$pkg"; then
|
|
MISSING+=("$pkg")
|
|
continue
|
|
fi
|
|
|
|
printf '%s\t%s\n' "$sym" "$mode" >> "$DESIRED"
|
|
PLANNED+=("$pkg: ${cur:-unset} -> $mode")
|
|
done <<< "$ENTRIES"
|
|
|
|
if [ ${#INVALID[@]} -gt 0 ]; then
|
|
warn "Ignored ${#INVALID[@]} entries with invalid package names:"
|
|
printf ' %s\n' "${INVALID[@]}"
|
|
fi
|
|
|
|
if [ ! -s "$DESIRED" ]; then
|
|
info "Nothing to change; .config already matches $PKG_LABEL."
|
|
if [ ${#MISSING[@]} -gt 0 ]; then
|
|
warn "Packages not found (check feeds or spelling):"
|
|
printf ' %s\n' "${MISSING[@]}"
|
|
warn "Try: ./scripts/feeds update -a && ./scripts/feeds install -a"
|
|
[ "$STRICT" = true ] && exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# --- Merge into .config ---------------------------------------------------
|
|
# Existing symbols are rewritten where they already are, so the file neither
|
|
# grows nor reorders; duplicates left behind by older versions collapse.
|
|
awk -F'\t' '
|
|
FNR == NR { want[$1] = $2; order[++nk] = $1; next }
|
|
{
|
|
key = ""
|
|
if ($0 ~ /^CONFIG_PACKAGE_[^=]+=/) { key = $0; sub(/=.*$/, "", key) }
|
|
else if ($0 ~ /^# CONFIG_PACKAGE_[^ ]+ is not set$/) {
|
|
# Extract by string, not by field: FS is a tab here, so $2 is empty
|
|
# on .config lines and the symbol would never be matched.
|
|
key = $0; sub(/^# /, "", key); sub(/ is not set$/, "", key)
|
|
}
|
|
if (key != "" && (key in want)) {
|
|
if (!(key in done)) {
|
|
done[key] = 1
|
|
if (want[key] == "n") print "# " key " is not set"
|
|
else print key "=" want[key]
|
|
}
|
|
next
|
|
}
|
|
print
|
|
}
|
|
END {
|
|
for (i = 1; i <= nk; i++) {
|
|
k = order[i]
|
|
if (k in done) continue
|
|
if (want[k] == "n") print "# " k " is not set"
|
|
else print k "=" want[k]
|
|
}
|
|
}
|
|
' "$DESIRED" .config > "$NEWCFG"
|
|
|
|
echo
|
|
echo "========================================"
|
|
echo " Planned .config changes"
|
|
echo "========================================"
|
|
printf ' %s\n' "${PLANNED[@]}"
|
|
echo " ---"
|
|
echo " change: ${#PLANNED[@]} unchanged: ${#SKIPPED[@]} not found: ${#MISSING[@]}"
|
|
echo "========================================"
|
|
|
|
if [ ${#MISSING[@]} -gt 0 ]; then
|
|
warn "Packages not found (check feeds or spelling):"
|
|
printf ' %s\n' "${MISSING[@]}"
|
|
warn "Try: ./scripts/feeds update -a && ./scripts/feeds install -a"
|
|
fi
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
echo
|
|
diff -u .config "$NEWCFG" || true
|
|
echo
|
|
info "Dry run complete; .config was not modified."
|
|
[ "$STRICT" = true ] && [ ${#MISSING[@]} -gt 0 ] && exit 1
|
|
exit 0
|
|
fi
|
|
|
|
echo
|
|
if ! confirm "Apply these changes to .config?" yes; then
|
|
info "Aborted by user."
|
|
exit 0
|
|
fi
|
|
|
|
[ "$KEEP_BACKUP" = true ] && cp -a .config .config.bak
|
|
mv -f "$NEWCFG" .config
|
|
chmod 644 .config
|
|
trap 'rm -f "$DESIRED"' EXIT
|
|
|
|
if [ "$RUN_DEFCONFIG" = true ]; then
|
|
info "Running make defconfig..."
|
|
make defconfig >/dev/null
|
|
|
|
# defconfig silently reverts a removal whose package something else selects,
|
|
# and can demote =y to =m. Without this check that stays invisible.
|
|
declare -a REVERTED=()
|
|
declare -A FINAL=()
|
|
while IFS= read -r line; do
|
|
case "$line" in
|
|
CONFIG_PACKAGE_*=*) FINAL["${line%%=*}"]="${line#*=}" ;;
|
|
"# CONFIG_PACKAGE_"*" is not set")
|
|
s="${line#\# }"
|
|
FINAL["${s% is not set}"]="n"
|
|
;;
|
|
esac
|
|
done < .config
|
|
|
|
while IFS=$'\t' read -r sym mode; do
|
|
got="${FINAL[$sym]:-unset}"
|
|
[ "$got" = "$mode" ] || REVERTED+=("${sym#CONFIG_PACKAGE_}: wanted =$mode, got =$got")
|
|
done < "$DESIRED"
|
|
|
|
if [ ${#REVERTED[@]} -gt 0 ]; then
|
|
warn "make defconfig changed ${#REVERTED[@]} of the requested states:"
|
|
printf ' %s\n' "${REVERTED[@]}"
|
|
warn "Usually a dependency pulled the package back in."
|
|
fi
|
|
fi
|
|
|
|
echo
|
|
echo "========================================"
|
|
echo "OK Applied ${#PLANNED[@]} change(s) from $PKG_LABEL"
|
|
[ "$KEEP_BACKUP" = true ] && echo " Backup: .config.bak"
|
|
echo "========================================"
|
|
|
|
if [ "$STRICT" = true ] && [ ${#MISSING[@]} -gt 0 ]; then
|
|
exit 1
|
|
fi
|