Files
openwrt-build-helper/AGENTS.md
Zhe Yuan 40d3b21e71 Add AGENTS.md and fill the gaps in the README
AGENTS.md describes the project for anyone -- human or agent -- changing a
script: what the repository is, where it sits inside the OpenWrt tree, which
scripts run from where, the shell conventions the existing code follows, and
the bash traps that have already caused bugs here.

README: document the options that were missing (--refresh, --skip-defconfig,
--extras-file/--no-extras-file, --extras, --wrap), distinguish --extras from
--extras-file, and replace the Debian-only Go install line with a
distribution-neutral description of how update-go-path.sh finds a toolchain.
Drop the hedged MIT claim, since no LICENSE file exists.

Both documents stay environment-neutral: no absolute home paths and no
assumption of a distribution, with platform-specific advice kept under an
explicit conditional heading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 10:59:41 -04:00

6.9 KiB

AGENTS.md

Guidance for coding agents working in this repository.

What this is

A set of Bash scripts that automate building OpenWrt from source: install the host dependencies, fetch the source tree, import a device's official configuration, and apply a per-device package list.

There is no build system, no test suite and no dependency manifest. The deliverable is the scripts. Everything is plain Bash plus curl/wget, git, awk, sed and python3 (only for reading JSON — jq is deliberately not required).

The governing design rule: a new OpenWrt release must not require editing any file here. Anything version specific is fetched from downloads.openwrt.org at run time; only the user's own package choices are stored in git. Reject changes that would reintroduce a version-pinned file, a hardcoded device table or a copied-in package list.

Repository layout

This repository is helper/ inside an OpenWrt source tree. prepare-openwrt.sh puts it there on first run:

<parent>/openwrt/                 OpenWrt source root (not this repository)
<parent>/openwrt/helper/          this repository
<parent>/openwrt-build-helper     symlink to openwrt/helper, for stale shells
helper/
  *.sh                            the scripts
  lib-openwrt-upstream.sh         shared library, sourced not executed
  packages/<device-id>.txt        tracked: the user's packages for one device
  .generated/                     ignored: combined lists, saved buildinfo
  README.md                       user-facing documentation

<device-id> is the official OpenWrt profile id (linksys_mx8500), so the filename, the --device argument and upstream's profiles.json key are always the same string.

Where the scripts run from

Two groups, and mixing them up is the most common mistake:

  • prepare-openwrt-env.sh and prepare-openwrt.sh run from this directory; they exist to create the OpenWrt tree, so they cannot assume one.
  • Everything else runs from the OpenWrt root (./helper/<script>.sh) because it reads or writes .config, feeds/, tmp/ or package/. Each of those guards its entry point — require_openwrt_root from the library, or an equivalent inline test for feeds.conf.default + Makefile. Keep that guard when adding a script to this group. The exception is gen-package-list.sh, which only produces text: it needs the tree for version detection (or --version) and delegates the root check to add-openwrt-packages.sh on --apply.

Never derive the OpenWrt root from $0's parent alone: the compat symlink means the script may be invoked through a path outside the tree. Resolve with pwd -P, and use find_openwrt_root when a version query must be anchored to the OpenWrt tree rather than to this repository's own git checkout.

The scripts

Script Role
prepare-openwrt-env.sh install host build dependencies; dispatches on the package manager (apt/dnf/yum/pacman/zypper/apk)
prepare-openwrt.sh clone or update the OpenWrt tree, relocate this repository into it
lib-openwrt-upstream.sh shared upstream access: URLs, caching, version detection, device lookup
download-config.sh install a device's official config.buildinfo as .config
gen-package-list.sh built-in packages from profiles.json + packages/<id>.txt
add-openwrt-packages.sh apply a package list to .config
update-go-path.sh point the build at an installed Go toolchain
add-external-repos.sh clone extra package repositories into package/

Only download-config.sh and gen-package-list.sh source the library; it is guarded against double-sourcing by _OWRT_LIB_LOADED.

Conventions

Match the existing style rather than introducing your own:

  • set -euo pipefail and umask 022 at the top of every executable script (add-external-repos.sh predates the convention and still uses bare set -e). The library sets nothing — the caller owns shell options.
  • The usage block is the file's own header comment, printed by usage() { sed -n '<start>,<end>p' "$0" | sed 's/^# \{0,1\}//;s/^#$//'; }. Editing the header changes --help — keep the line range correct, and update the header whenever you add an option.
  • All logging goes to stderr (info/warn/err). Functions return their value on stdout, so a stray echo to stdout corrupts VAR="$(fn)" at the call site. This has broken the code before.
  • Every destructive command goes through run, so --dry-run prints instead of acting; every prompt goes through confirm, so -y skips it. Both honour the DRY_RUN / ASSUME_YES globals.
  • Read-only discovery first, mutation after the confirmation prompt. Aborting at the prompt must leave the filesystem untouched.
  • Write to a temp file and validate before replacing something that matters; keep a .bak.
  • Tunables are ${VAR:-default} so they can be overridden from the environment without editing the file.

Bash traps that have actually bitten this code:

  • local a="$1" b="$a" — bash expands every word before assigning any, so $a is unset under set -u. Use one local per dependent variable.
  • nullglob only drops words that contain a wildcard; a literal path such as /usr/local/go/ survives even when it does not exist. Check with -d.
  • cmd | grep -q under pipefail can fail on SIGPIPE. Prefer a pipe-free form.
  • In awk -F'\t', $2 is not whitespace-split. Extract with string operations when the field separator is not space.
  • git checkout <ref> is a no-op when HEAD is already at <ref>, so it does not restore deleted tracked files.

Data files

packages/<device-id>.txt is hand written and holds only the user's extra packages — never the built-in set, which is always generated. Format:

curl luci-app-ttyd     whitespace separated, any number per line, applied as =y
-luci-app-wol          leading '-' removes a package (=n)
##module               following packages are set to =m
##remove               ... =n
##built-in             ... back to =y (the default)
pkg  # note            text after a single '#' is a comment

.generated/ is git-ignored build output. Do not commit it, and do not make anything depend on a file in it existing.

Checking your work

There is no test runner. Before committing:

  • bash -n <script> on everything you touched.
  • shellcheck if available.
  • Run the script with -n/--dry-run and with -h; confirm -h still prints the full option list (the sed line range drifts silently).
  • Behaviour-changing edits to config handling deserve a real check against a concrete device: generate before and after, and diff the resulting sets of =y packages. Do not claim equivalence you have not diffed.

Assume the user's machine is not the only target: no absolute home paths, no assumption of a distro, an init system, or WSL. Platform-specific advice belongs in the README under an explicit conditional heading.