opkg_check() and every opkg/apk invocation hardcoded /usr/bin/opkg and /usr/bin/apk. Official OpenWrt images don't all symlink /bin into /usr/bin — the glinet_gl-mt3000 24.10.2 build keeps them as separate real directories with opkg living in /bin — so the check silently missed a perfectly normal install and TollGate provisioning failed with "this router's firmware may not support package management". Switched every call to resolve through the router's own $PATH (command -v / bare opkg / apk) instead. Reproduced and fixed live against archy-x250-pa3, 2026-09-05. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0176RpCxFNS9ZaSJjL72W9Z5
132 lines
5.2 KiB
Rust
132 lines
5.2 KiB
Rust
use anyhow::Result;
|
|
use tracing::info;
|
|
|
|
use crate::Router;
|
|
|
|
/// Which package manager is available on this router.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum PkgManager {
|
|
/// Traditional opkg (OpenWrt ≤24.x).
|
|
Opkg,
|
|
/// OpenWrt 25.x+ — apk is the native manager, opkg is not in repos.
|
|
ApkNative,
|
|
}
|
|
|
|
impl Router {
|
|
/// Detect which package manager is available.
|
|
///
|
|
/// Looks up `opkg`/`apk` via the router's `$PATH` (`command -v`) rather
|
|
/// than a hardcoded `/usr/bin/<tool>` — official OpenWrt images don't all
|
|
/// symlink `/bin` into `/usr/bin` (e.g. the `glinet_gl-mt3000` 24.10.2
|
|
/// build keeps them as separate real directories with `opkg` living in
|
|
/// `/bin`), so a fixed absolute path silently misses a perfectly normal
|
|
/// install and reports "no package management" (archy-x250-pa3, 2026-09-05).
|
|
///
|
|
/// - If `opkg` is on PATH → `PkgManager::Opkg` (nothing to do).
|
|
/// - If `apk` is on PATH → run `apk update` (switching repos to HTTP
|
|
/// first to work around missing CA bundle on fresh images), then try
|
|
/// `apk add opkg`. If opkg is in the repos → `Opkg`. If not (OpenWrt
|
|
/// 25.x) → `ApkNative`.
|
|
/// - Neither found → error.
|
|
pub fn opkg_check(&self) -> Result<PkgManager> {
|
|
let (_, code) = self.run("command -v opkg >/dev/null 2>&1")?;
|
|
if code == 0 {
|
|
return Ok(PkgManager::Opkg);
|
|
}
|
|
|
|
let (_, apk_code) = self.run("command -v apk >/dev/null 2>&1")?;
|
|
if apk_code == 0 {
|
|
info!("[{}] opkg not found — using apk (OpenWrt 25.x+)", self.host);
|
|
// Fresh images ship without a CA bundle; switch repos to HTTP so
|
|
// apk's wget can reach the package index without TLS verification.
|
|
self.run_ok("sed -i 's|https://|http://|g' /etc/apk/repositories 2>/dev/null || true")?;
|
|
let (update_out, update_code) = self.run("apk update 2>&1")?;
|
|
if update_code != 0 {
|
|
anyhow::bail!(
|
|
"apk update failed (exit {}) — router may have no internet access. \
|
|
Ensure WAN/internet is working on the router before provisioning.\n{}",
|
|
update_code,
|
|
update_out.trim()
|
|
);
|
|
}
|
|
// Try to install opkg (only available on some 25.x builds).
|
|
let (add_out, add_code) = self.run("apk add opkg 2>&1")?;
|
|
if add_code == 0 {
|
|
return Ok(PkgManager::Opkg);
|
|
}
|
|
if add_out.contains("no such package") || add_out.contains("unable to select") {
|
|
info!(
|
|
"[{}] opkg not in apk repos — staying in apk-native mode",
|
|
self.host
|
|
);
|
|
return Ok(PkgManager::ApkNative);
|
|
}
|
|
anyhow::bail!(
|
|
"apk add opkg failed (exit {}): {}",
|
|
add_code,
|
|
add_out.trim()
|
|
);
|
|
}
|
|
|
|
anyhow::bail!(
|
|
"Neither opkg nor apk found on this router's $PATH — its firmware may not \
|
|
support package management (TollGate requires a standard OpenWrt build)"
|
|
);
|
|
}
|
|
|
|
/// `opkg update` — refresh package lists.
|
|
pub fn opkg_update(&self) -> Result<()> {
|
|
info!("[{}] opkg update", self.host);
|
|
self.run_ok("opkg update")?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Install a package, skipping if already installed.
|
|
pub fn opkg_install(&self, package: &str) -> Result<()> {
|
|
// Check if already installed to avoid unnecessary network traffic.
|
|
let (_, code) = self.run(&format!("opkg list-installed | grep -q '^{} '", package))?;
|
|
if code == 0 {
|
|
info!("[{}] {} already installed", self.host, package);
|
|
return Ok(());
|
|
}
|
|
|
|
info!("[{}] opkg install {}", self.host, package);
|
|
self.run_ok(&format!("opkg install {}", package))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove a package.
|
|
pub fn opkg_remove(&self, package: &str) -> Result<()> {
|
|
info!("[{}] opkg remove {}", self.host, package);
|
|
self.run_ok(&format!("opkg remove {}", package))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Install a standard OpenWrt package via whichever manager is active.
|
|
///
|
|
/// Unlike `tollgate-module-basic-go` itself (which falls back to manual
|
|
/// `.ipk` extraction and therefore skips dependency resolution — see
|
|
/// `tollgate::install`), packages like `nodogsplash` are in every
|
|
/// upstream OpenWrt feed, so a plain `apk add` / `opkg install` works
|
|
/// even in `ApkNative` mode.
|
|
pub fn install_package(&self, pkg_mgr: PkgManager, package: &str) -> Result<()> {
|
|
match pkg_mgr {
|
|
PkgManager::Opkg => self.opkg_install(package),
|
|
PkgManager::ApkNative => self.apk_install(package),
|
|
}
|
|
}
|
|
|
|
/// `apk add <package>`, skipping if already installed.
|
|
pub fn apk_install(&self, package: &str) -> Result<()> {
|
|
let (_, code) = self.run(&format!("apk info -e {} >/dev/null 2>&1", package))?;
|
|
if code == 0 {
|
|
info!("[{}] {} already installed", self.host, package);
|
|
return Ok(());
|
|
}
|
|
|
|
info!("[{}] apk add {}", self.host, package);
|
|
self.run_ok(&format!("apk add {}", package))?;
|
|
Ok(())
|
|
}
|
|
}
|