-
-
Save jamesmacwhite/7f6463ae7b307692c26a81be5cddbdcf to your computer and use it in GitHub Desktop.
| #!/bin/sh | |
| # mwan3-nft-update.sh | |
| # | |
| # Query GitHub releases for https://github.com/dl12345/mwan3 (and, | |
| # optionally, its companion https://github.com/dl12345/luci-app-mwan3), | |
| # find the .apk asset(s) matching this router, download to /tmp and | |
| # install with apk. | |
| # | |
| # Target: OpenWrt 25.12 (apk-based package manager). POSIX/ash only, | |
| # no bash-isms, no jq/curl assumed present. | |
| # | |
| # mwan3 release assets are per-arch, e.g.: | |
| # mwan3-3.6.11-1_openwrt-25.12_aarch64_cortex-a53.apk | |
| # mwan3-3.6.11-1_openwrt-25.12_x86_64.apk | |
| # mwan3-3.6.11-1_openwrt-25.12_arm_cortex-a7.apk | |
| # mwan3-3.6.11-1_openwrt-25.12_arm_cortex-a7_neon-vfpv4.apk | |
| # ... | |
| # | |
| # luci-app-mwan3 release assets are arch-independent (LuCI is Lua/JS), | |
| # one per release, e.g.: | |
| # luci-app-mwan3_26.999.3.6.10.apk | |
| # | |
| # Usage: | |
| # mwan3-nft-update.sh [options] | |
| # | |
| # Options: | |
| # -c, --check Only check for updates, don't download/install | |
| # -f, --force Reinstall even if already up to date, and allow | |
| # downgrading if the installed version (e.g. a | |
| # beta) is newer than the latest matched release | |
| # -k, --keep Keep the downloaded .apk file(s) in /tmp after install | |
| # -p, --pre-release Also consider pre-releases (default: stable releases only) | |
| # -l, --luci Also fetch and install the companion luci-app-mwan3 package | |
| # -a, --arch ARCH Override detected arch (skip /etc/apk/arch lookup) | |
| # -o, --openwrt-ver VER OpenWrt version string to match in the mwan3 asset name (default: auto-detected, falls back to 25.12) | |
| # -r, --repo OWNER/REPO Override the mwan3 GitHub repo (default: dl12345/mwan3) | |
| # --luci-repo OWNER/REPO Override the luci-app-mwan3 GitHub repo (default: dl12345/luci-app-mwan3) | |
| # -h, --help Show this help and exit | |
| # | |
| # Notes: | |
| # - These packages are not part of the official OpenWrt feed, so they | |
| # are unsigned as far as this router's apk keyring is concerned. | |
| # The install step uses `apk add --allow-untrusted`. Only run this | |
| # against repos you trust. | |
| # - Unauthenticated GitHub API calls are rate-limited to 60/hour per | |
| # source IP. Set GITHUB_TOKEN in the environment to raise that limit | |
| # if you're scheduling this via cron. | |
| set -eu | |
| REPO="dl12345/mwan3" | |
| LUCI_REPO="dl12345/luci-app-mwan3" | |
| TMP_DIR="/tmp" | |
| CHECK_ONLY=0 | |
| FORCE=0 | |
| KEEP_FILE=0 | |
| PRE_RELEASE=0 | |
| INSTALL_LUCI=0 | |
| ARCH_OVERRIDE="" | |
| OPENWRT_VER_OVERRIDE="" | |
| PKG_NAME="mwan3" | |
| LUCI_PKG_NAME="luci-app-mwan3" | |
| log() { printf '%s\n' "$*" >&2; } | |
| die() { log "ERROR: $*"; exit 1; } | |
| usage() { | |
| sed -n '2,/^set -eu/p' "$0" | sed 's/^# \{0,1\}//' | sed '$d' | |
| } | |
| # --------------------------------------------------------------------- | |
| # Argument parsing | |
| # --------------------------------------------------------------------- | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| -c|--check) CHECK_ONLY=1 ;; | |
| -f|--force) FORCE=1 ;; | |
| -k|--keep) KEEP_FILE=1 ;; | |
| -p|--pre-release) PRE_RELEASE=1 ;; | |
| -l|--luci) INSTALL_LUCI=1 ;; | |
| -a|--arch) ARCH_OVERRIDE="${2:?--arch requires a value}"; shift ;; | |
| -o|--openwrt-ver) OPENWRT_VER_OVERRIDE="${2:?--openwrt-ver requires a value}"; shift ;; | |
| -r|--repo) REPO="${2:?--repo requires a value}"; shift ;; | |
| --luci-repo) LUCI_REPO="${2:?--luci-repo requires a value}"; shift ;; | |
| -h|--help) usage; exit 0 ;; | |
| *) die "Unknown argument: $1 (use -h for help)" ;; | |
| esac | |
| shift | |
| done | |
| # --------------------------------------------------------------------- | |
| # Pick a downloader: curl > wget (GNU/wget-ssl) > uclient-fetch | |
| # --------------------------------------------------------------------- | |
| if command -v curl >/dev/null 2>&1; then | |
| FETCHER="curl" | |
| elif command -v wget >/dev/null 2>&1; then | |
| FETCHER="wget" | |
| elif command -v uclient-fetch >/dev/null 2>&1; then | |
| FETCHER="uclient-fetch" | |
| else | |
| die "No downloader found (need curl, wget, or uclient-fetch)." | |
| fi | |
| # fetch_stdout URL -> prints response body to stdout | |
| fetch_stdout() { | |
| url="$1" | |
| case "$FETCHER" in | |
| curl) | |
| curl_hdrs="" | |
| [ -n "${GITHUB_TOKEN:-}" ] && curl_hdrs="-H Authorization:\ token\ $GITHUB_TOKEN" | |
| # shellcheck disable=SC2086 | |
| curl -fsSL -H "Accept: application/vnd.github+json" $curl_hdrs "$url" | |
| ;; | |
| wget) | |
| wget_hdrs="" | |
| [ -n "${GITHUB_TOKEN:-}" ] && wget_hdrs="--header=Authorization:\ token\ $GITHUB_TOKEN" | |
| # shellcheck disable=SC2086 | |
| wget -q -O - --header="Accept: application/vnd.github+json" $wget_hdrs "$url" | |
| ;; | |
| uclient-fetch) | |
| uclient-fetch -qO - "$url" | |
| ;; | |
| esac | |
| } | |
| # vercmp A B -> echoes "lt", "eq", "gt" (A vs B) or "unknown" if it can't | |
| # be determined. Delegates to `apk version -t`, the same comparator apk | |
| # itself uses (handles suffixes like _beta/_rc/-rN correctly, so e.g. | |
| # 3.7_beta-r1 correctly compares as newer than 3.6.11-1). | |
| vercmp() { | |
| a="$1" | |
| b="$2" | |
| if command -v apk >/dev/null 2>&1; then | |
| r=$(apk version -t "$a" "$b" 2>/dev/null) || r="" | |
| case "$r" in | |
| "<") echo "lt"; return ;; | |
| "=") echo "eq"; return ;; | |
| ">") echo "gt"; return ;; | |
| esac | |
| fi | |
| if [ "$a" = "$b" ]; then | |
| echo "eq" | |
| else | |
| echo "unknown" | |
| fi | |
| } | |
| # is_prerelease_str VERSION -> true if VERSION looks like an | |
| # alpha/beta/pre/rc build. | |
| # | |
| # These repos tag stable releases as "<ver>-<n>" (e.g. 3.6.11-1), which | |
| # is NOT apk's own "-r<n>" revision syntax, so it doesn't reliably parse | |
| # as a well-formed apk version. Beta/dev builds installed out-of-band | |
| # (e.g. 3.7_beta-r1) DO happen to match apk's format. Comparing a | |
| # well-formed version against a malformed one via `apk version -t` gives | |
| # undefined/inconsistent results - in practice this can make a beta look | |
| # "older" than a subsequent stable release and get silently downgraded. | |
| # To stay safe, a pre-release marker in the installed version always | |
| # wins over a freshly-fetched stable release, regardless of what the | |
| # numeric comparison says. | |
| is_prerelease_str() { | |
| case "$1" in | |
| *_alpha*|*_beta*|*_pre*|*_rc*|*-alpha*|*-beta*|*-rc*) return 0 ;; | |
| *) return 1 ;; | |
| esac | |
| } | |
| # fetch_file URL DEST -> downloads to DEST, returns non-zero on failure | |
| fetch_file() { | |
| url="$1" | |
| dest="$2" | |
| case "$FETCHER" in | |
| curl) | |
| curl -fsSL -o "$dest" "$url" | |
| ;; | |
| wget) | |
| wget -q -O "$dest" "$url" | |
| ;; | |
| uclient-fetch) | |
| uclient-fetch -qO "$dest" "$url" | |
| ;; | |
| esac | |
| } | |
| # --------------------------------------------------------------------- | |
| # Detect router arch (from apk, the 25.12 package manager) | |
| # --------------------------------------------------------------------- | |
| detect_arch() { | |
| if [ -n "$ARCH_OVERRIDE" ]; then | |
| printf '%s' "$ARCH_OVERRIDE" | |
| return | |
| fi | |
| if [ -r /etc/apk/arch ]; then | |
| cat /etc/apk/arch | |
| return | |
| fi | |
| die "Could not read /etc/apk/arch. Pass --arch explicitly (e.g. aarch64_cortex-a53)." | |
| } | |
| # --------------------------------------------------------------------- | |
| # Detect OpenWrt major.minor version, e.g. "25.12" | |
| # --------------------------------------------------------------------- | |
| detect_openwrt_ver() { | |
| if [ -n "$OPENWRT_VER_OVERRIDE" ]; then | |
| printf '%s' "$OPENWRT_VER_OVERRIDE" | |
| return | |
| fi | |
| if [ -r /etc/openwrt_release ]; then | |
| rel=$(sed -n "s/^DISTRIB_RELEASE='\{0,1\}\([^'\"]*\)'\{0,1\}/\1/p" /etc/openwrt_release | head -n1) | |
| [ -n "$rel" ] && { echo "$rel" | cut -d. -f1,2; return; } | |
| fi | |
| # fallback: assume the version this script was written for | |
| printf '25.12' | |
| } | |
| ARCH=$(detect_arch) | |
| OPENWRT_VER=$(detect_openwrt_ver) | |
| [ -n "$ARCH" ] || die "Detected arch is empty." | |
| log "Router arch: $ARCH" | |
| log "OpenWrt version: $OPENWRT_VER" | |
| # --------------------------------------------------------------------- | |
| # process_pkg REPO PKG_NAME MODE LABEL | |
| # | |
| # REPO owner/repo on GitHub | |
| # PKG_NAME apk package name (used to match the installed version and, | |
| # for noarch packages, to match the release asset) | |
| # MODE "arch" - match asset by "_<ARCH>.apk" (mwan3 core pkg) | |
| # "noarch" - match the single arch-independent asset | |
| # (luci-app-mwan3) | |
| # LABEL human-readable label for log output | |
| # | |
| # /releases/latest only ever returns the newest non-prerelease, | |
| # non-draft release. To also consider pre-releases we instead pull the | |
| # single newest entry from /releases (sorted newest-first, drafts are | |
| # never visible unauthenticated), which may be a prerelease. | |
| # --------------------------------------------------------------------- | |
| process_pkg() { | |
| repo="$1" | |
| pkg_name="$2" | |
| mode="$3" | |
| label="$4" | |
| log "" | |
| log "== $label ($repo) ==" | |
| if [ "$PRE_RELEASE" -eq 1 ]; then | |
| api_url="https://api.github.com/repos/${repo}/releases?per_page=1" | |
| log "Querying $api_url (including pre-releases) ..." | |
| else | |
| api_url="https://api.github.com/repos/${repo}/releases/latest" | |
| log "Querying $api_url ..." | |
| fi | |
| rel_json=$(fetch_stdout "$api_url") || die "Failed to query GitHub API for $repo." | |
| [ -n "$rel_json" ] || die "Empty response from GitHub API for $repo." | |
| trimmed_json=$(printf '%s' "$rel_json" | tr -d ' \t\n\r') | |
| if [ "$trimmed_json" = "[]" ]; then | |
| die "No releases found for $repo." | |
| fi | |
| # GitHub errors come back as JSON with a "message" field and no assets. | |
| if ! printf '%s' "$rel_json" | grep -q '"tag_name"'; then | |
| err_msg=$(printf '%s' "$rel_json" | sed -n 's/.*"message": *"\([^"]*\)".*/\1/p' | head -n1) | |
| die "GitHub API did not return a release for $repo. ${err_msg:-Unexpected response.}" | |
| fi | |
| tag=$(printf '%s' "$rel_json" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n1) | |
| [ -n "$tag" ] || die "Could not parse tag_name from release JSON for $repo." | |
| is_prerelease=$(printf '%s' "$rel_json" | sed -n 's/.*"prerelease": *\(true\|false\).*/\1/p' | head -n1) | |
| if [ "$is_prerelease" = "true" ]; then | |
| log "Latest release: $tag (pre-release)" | |
| else | |
| log "Latest release: $tag" | |
| fi | |
| if [ "$mode" = "arch" ]; then | |
| # Match on "_<arch>.apk" at the end of the filename so e.g. | |
| # arm_cortex-a7 doesn't also match arm_cortex-a7_neon-vfpv4. | |
| asset_url=$(printf '%s' "$rel_json" \ | |
| | grep '"browser_download_url"' \ | |
| | grep "openwrt-${OPENWRT_VER}_${ARCH}\.apk\"" \ | |
| | sed -n 's/.*"browser_download_url": *"\([^"]*\)".*/\1/p' \ | |
| | head -n1) | |
| if [ -z "$asset_url" ]; then | |
| log "No asset matched 'openwrt-${OPENWRT_VER}_${ARCH}.apk', retrying without the OpenWrt version filter..." | |
| asset_url=$(printf '%s' "$rel_json" \ | |
| | grep '"browser_download_url"' \ | |
| | grep "_${ARCH}\.apk\"" \ | |
| | sed -n 's/.*"browser_download_url": *"\([^"]*\)".*/\1/p' \ | |
| | head -n1) | |
| fi | |
| [ -n "$asset_url" ] || die "No release asset found for arch '$ARCH' in $repo $tag." | |
| new_ver=$(printf '%s' "$tag" | sed 's/^v//') | |
| else | |
| # noarch: exactly one asset, named "<pkg_name>_<version>.apk" | |
| asset_url=$(printf '%s' "$rel_json" \ | |
| | grep '"browser_download_url"' \ | |
| | grep "${pkg_name}_.*\.apk\"" \ | |
| | sed -n 's/.*"browser_download_url": *"\([^"]*\)".*/\1/p' \ | |
| | head -n1) | |
| [ -n "$asset_url" ] || die "No release asset found for $repo $tag (expected a single arch-independent .apk)." | |
| asset_base=$(basename "$asset_url") | |
| new_ver=$(printf '%s' "$asset_base" | sed "s/^${pkg_name}_//; s/\.apk\$//") | |
| fi | |
| asset_name=$(basename "$asset_url") | |
| log "Matched asset: $asset_name" | |
| installed_ver="" | |
| if command -v apk >/dev/null 2>&1; then | |
| installed_ver=$(apk list -I 2>/dev/null | sed -n "s/^${pkg_name}-\([^ ]*\) .*/\1/p" | head -n1) | |
| fi | |
| if [ -n "$installed_ver" ]; then | |
| log "Installed version: $installed_ver" | |
| else | |
| log "Installed version: (not installed)" | |
| fi | |
| if [ -z "$installed_ver" ]; then | |
| cmp="lt" | |
| elif is_prerelease_str "$installed_ver" && [ "$is_prerelease" != "true" ]; then | |
| # Installed looks like an alpha/beta/rc build and the fetched | |
| # release is a stable one - never auto-"update" over that (see | |
| # is_prerelease_str for why we don't trust vercmp here). | |
| log "Installed version ($installed_ver) looks like a pre-release build; the fetched release ($new_ver) is stable, so it's not being treated as an update." | |
| cmp="gt" | |
| else | |
| cmp=$(vercmp "$installed_ver" "$new_ver") | |
| fi | |
| case "$cmp" in | |
| eq) | |
| log "Already up to date." | |
| if [ "$CHECK_ONLY" -eq 1 ]; then | |
| return 0 | |
| fi | |
| if [ "$FORCE" -ne 1 ]; then | |
| log "Skipping install (use --force to reinstall anyway)." | |
| return 0 | |
| fi | |
| log "--force given: reinstalling $new_ver." | |
| ;; | |
| gt) | |
| log "Installed version ($installed_ver) is newer than the latest release ($new_ver) - not downgrading." | |
| if [ "$CHECK_ONLY" -eq 1 ]; then | |
| return 0 | |
| fi | |
| if [ "$FORCE" -ne 1 ]; then | |
| log "Skipping install (use --force to install anyway)." | |
| return 0 | |
| fi | |
| log "--force given: proceeding with downgrade to $new_ver." | |
| ;; | |
| lt|unknown) | |
| if [ "$cmp" = "unknown" ]; then | |
| log "Could not determine version ordering (apk not available?) - treating as update available." | |
| fi | |
| if [ "$CHECK_ONLY" -eq 1 ]; then | |
| log "Update available: ${installed_ver:-none} -> $new_ver" | |
| return 0 | |
| fi | |
| ;; | |
| esac | |
| dest="${TMP_DIR}/${asset_name}" | |
| log "Downloading to $dest ..." | |
| fetch_file "$asset_url" "$dest" || die "Download failed for $repo." | |
| [ -s "$dest" ] || die "Downloaded file is empty: $dest" | |
| log "Installing $asset_name (unsigned/third-party package: using --allow-untrusted) ..." | |
| install_ok=1 | |
| apk add --allow-untrusted "$dest" || install_ok=0 | |
| if [ "$KEEP_FILE" -ne 1 ]; then | |
| rm -f "$dest" | |
| fi | |
| [ "$install_ok" -eq 1 ] || die "apk install failed for $repo." | |
| log "Done. Installed $pkg_name $new_ver." | |
| } | |
| process_pkg "$REPO" "$PKG_NAME" "arch" "mwan3 (core)" | |
| if [ "$INSTALL_LUCI" -eq 1 ]; then | |
| process_pkg "$LUCI_REPO" "$LUCI_PKG_NAME" "noarch" "luci-app-mwan3 (LuCI UI)" | |
| fi |
I encountered an error trying to install using this script.
root@OpenWrt:/tmp# wget https://gist.githubusercontent.com/jamesmacwhite/7f6463ae7b307692c26a81be5cddbdcf/raw/70088070644b1a95222f14d2f4b86d6bf7fdec32/mwan3-nft-u pdate.sh -O mwan3-nft-update.sh Downloading 'https://gist.githubusercontent.com/jamesmacwhite/7f6463ae7b307692c26a81be5cddbdcf/raw/70088070644b1a95222f14d2f4b86d6bf7fdec32/mwan3-nft-update.sh' Connecting to 185.199.111.133:443 Writing to 'mwan3-nft-update.sh' mwan3-nft-update.sh 100% |*******************************| 11708 0:00:00 ETA Download completed (11708 bytes) root@OpenWrt:/tmp# chmod +x /tmp/mwan3-nft-update.sh root@OpenWrt:/tmp# ./mwan3-nft-update.sh Router arch: aarch64_cortex-a53 OpenWrt version: 25.12 == mwan3 (core) (dl12345/mwan3) == Querying https://api.github.com/repos/dl12345/mwan3/releases/latest ... Latest release: v3.6.12-1 Matched asset: mwan3-3.6.12-1_openwrt-25.12_x86_64.apk Installed version: (not installed) Downloading to /tmp/mwan3-3.6.12-1_openwrt-25.12_x86_64.apk ... Installing mwan3-3.6.12-1_openwrt-25.12_x86_64.apk (unsigned/third-party package: using --allow-untrusted) ... ERROR: unable to select packages: mwan3-3.6.12-r1: error: uninstallable arch: x86_64 satisfies: world[mwan3><Q1TtGJpa8fikyE94MrSpyb97qbDX8=] ERROR: apk install failed for dl12345/mwan3.My
Router archisaarch64_cortex-a53but the matched asset is anx86_64package, causing the installation error.
Arch detection can fail in some cases, if you know your arch, you can force it via the -a or --arch parameter which will force looking up the GitHub releases and bypass the arch detection logic.
It is a bit of a stop gap currently, until the package can be made available in the official OpenWrt feeds/custom feed.
I encountered an error trying to install using this script.
root@OpenWrt:/tmp# wget https://gist.githubusercontent.com/jamesmacwhite/7f6463ae7b307692c26a81be5cddbdcf/raw/70088070644b1a95222f14d2f4b86d6bf7fdec32/mwan3-nft-u pdate.sh -O mwan3-nft-update.sh Downloading 'https://gist.githubusercontent.com/jamesmacwhite/7f6463ae7b307692c26a81be5cddbdcf/raw/70088070644b1a95222f14d2f4b86d6bf7fdec32/mwan3-nft-update.sh' Connecting to 185.199.111.133:443 Writing to 'mwan3-nft-update.sh' mwan3-nft-update.sh 100% |*******************************| 11708 0:00:00 ETA Download completed (11708 bytes) root@OpenWrt:/tmp# chmod +x /tmp/mwan3-nft-update.sh root@OpenWrt:/tmp# ./mwan3-nft-update.sh Router arch: aarch64_cortex-a53 OpenWrt version: 25.12 == mwan3 (core) (dl12345/mwan3) == Querying https://api.github.com/repos/dl12345/mwan3/releases/latest ... Latest release: v3.6.12-1 Matched asset: mwan3-3.6.12-1_openwrt-25.12_x86_64.apk Installed version: (not installed) Downloading to /tmp/mwan3-3.6.12-1_openwrt-25.12_x86_64.apk ... Installing mwan3-3.6.12-1_openwrt-25.12_x86_64.apk (unsigned/third-party package: using --allow-untrusted) ... ERROR: unable to select packages: mwan3-3.6.12-r1: error: uninstallable arch: x86_64 satisfies: world[mwan3><Q1TtGJpa8fikyE94MrSpyb97qbDX8=] ERROR: apk install failed for dl12345/mwan3.My
Router archisaarch64_cortex-a53but the matched asset is anx86_64package, causing the installation error.Arch detection can fail in some cases, if you know your arch, you can force it via the
-aor--archparameter which will force looking up the GitHub releases and bypass the arch detection logic.It is a bit of a stop gap currently, until the package can be made available in the official OpenWrt feeds/custom feed.
Arch detection detected my system correctly, it's the matching logic that is the problem, that it always matches x86_64 which is the last item in the list.
quick fix: replace line 282 with
| grep -o '"browser_download_url": *"[^"]*"' \
I encountered an error trying to install using this script.
My
Router archisaarch64_cortex-a53but the matched asset is anx86_64package, causing the installation error.