From 2f049feed29b9d27c5a6fef7f3e405cf822aa319 Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Thu, 5 Feb 2026 02:00:36 +1000 Subject: [PATCH] Install script, static assets, UI and release workflow updates - install.sh: default dirs, --update/--service/--uninstall/--purge, in-dir dnsmasq-webui symlink, ContentRootPath note - UseStaticFiles instead of MapStaticAssets (fix 0-byte assets) - Program: ContentRootPath when not Development for symlink/CWD - App.razor: direct asset paths - Dnsmasq page: remove redundant status pre, margin above Reload button - Sidebar: logo 64px via app.css, hamburger vertically centered, toggler inside top-row - ApplicationTitle documented in README - publish/release: RIDs, Docker build for glibc --- .github/workflows/release.yml | 18 +- README.md | 16 +- scripts/install.sh | 167 +++++++++++++++--- scripts/publish-self-contained.sh | 18 +- src/DnsmasqWebUI/Components/App.razor | 9 +- .../Components/Layout/NavMenu.razor | 5 +- .../Components/Layout/NavMenu.razor.css | 12 +- .../Components/Pages/Dnsmasq.razor | 6 +- src/DnsmasqWebUI/DnsmasqWebUI.csproj | 3 + src/DnsmasqWebUI/Program.cs | 9 +- src/DnsmasqWebUI/wwwroot/app.css | 18 ++ testdata/dnsmasq-test.conf | 6 +- testdata/dnsmasq.conf | 4 +- 13 files changed, 228 insertions(+), 63 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 14ebc6f..6bd21a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,7 @@ -# Build self-contained binaries for multiple RIDs when a version tag is pushed, +# Build self-contained binaries for portable Linux RIDs when a version tag is pushed, # then create/update the release and attach the zips as release assets. +# linux-x64 and linux-arm64 are built inside a container (Debian-based SDK image) so +# the resulting binaries work across glibc-based distros (Ubuntu, Debian, etc.). # Run: push a tag (e.g. v1.0.0); the workflow builds that commit and uploads assets. name: Release @@ -16,20 +18,28 @@ jobs: rid: - linux-x64 - linux-arm64 - - ubuntu.24.04-x64 - - ubuntu.22.04-x64 - linux-musl-x64 - linux-musl-arm64 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Publish for ${{ matrix.rid }} (glibc in Docker) + if: matrix.rid == 'linux-x64' || matrix.rid == 'linux-arm64' + run: | + docker run --rm \ + -v "${{ github.workspace }}:/src" -w /src \ + mcr.microsoft.com/dotnet/sdk:9.0 \ + ./scripts/publish-self-contained.sh ${{ matrix.rid }} + - name: Setup .NET + if: matrix.rid == 'linux-musl-x64' || matrix.rid == 'linux-musl-arm64' uses: actions/setup-dotnet@v4 with: dotnet-version: '9.0.x' - - name: Publish for ${{ matrix.rid }} + - name: Publish for ${{ matrix.rid }} (musl on host) + if: matrix.rid == 'linux-musl-x64' || matrix.rid == 'linux-musl-arm64' run: ./scripts/publish-self-contained.sh ${{ matrix.rid }} - name: Zip publish output diff --git a/README.md b/README.md index fa84a25..f943f8c 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ By default the script installs to **`~/.local/share/dnsmasq-webui`** and creates ```bash dnsmasq-webui # or -~/.local/share/dnsmasq-webui/DnsmasqWebUI +~/.local/share/dnsmasq-webui/dnsmasq-webui ``` **System-wide install (requires sudo):** @@ -98,6 +98,8 @@ sudo ./scripts/install.sh --uninstall --purge --system # Also remove /opt/dnsm **Installing from a fork:** Pass the repo explicitly: `./scripts/install.sh --repo owner/dnsmasq-webui` or set `GITHUB_REPO=owner/dnsmasq-webui`. The default repo is `alexhopeoconnor/dnsmasq-webui`. +**If the binary fails to run** (e.g. `TypeLoadException` or glibc errors): use the install script with `--build-from-source` so the app is built for your machine. This requires the .NET SDK and a **git clone** (it does not work when installing via `curl ... | sh`). Clone the repo, then run `./scripts/install.sh --build-from-source`. The script checks that .NET is installed, detects your OS/arch, and builds (on Ubuntu it may use a distro-specific RID for a better match). Other distros use the portable RID and may still hit runtime issues; building from source on the target machine is the most reliable. + **Configuration:** Set at least `Dnsmasq__MainConfigPath` to your main dnsmasq config. See [Configuration](#configuration) for all options. @@ -200,9 +202,19 @@ The app is configured via **appsettings.json**, **environment variables**, and * | `StatusShowCommand` | Optional: full status output (e.g. systemctl status) | `systemctl status dnsmasq --no-pager` | | `LogsCommand` | Optional: recent logs (e.g. journalctl) | `journalctl -u dnsmasq -n 100 --no-pager` | +**Application options** (use `Application__` prefix for env, `Application` section in JSON): + +| Option | Description | Example | +|--------|-------------|---------| +| `ApplicationTitle` | Title shown in the sidebar brand and browser tab (default: "Local DNS") | `"ApplicationTitle": "My DNS"` or `Application__ApplicationTitle=My DNS` | + **Host / URLs:** `ASPNETCORE_URLS=http://0.0.0.0:8080` or `--urls=http://0.0.0.0:8080` to bind to a specific address and port. -Place `appsettings.json` in the same directory as the executable (or use the default project paths when running with `dotnet run`). +Place `appsettings.json` in the same directory as the executable (or use the default project paths when running with `dotnet run`). You can override any option via environment variables using the `Dnsmasq__` prefix (e.g. `Dnsmasq__MainConfigPath=/path/to/dnsmasq.conf`). To use the repo test config: + +```bash +Dnsmasq__MainConfigPath=/path/to/dnsmasq-webui/testdata/dnsmasq-test.conf dnsmasq-webui +``` --- diff --git a/scripts/install.sh b/scripts/install.sh index 4b0d5f7..fe67247 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -16,6 +16,7 @@ SERVICE=false UNINSTALL=false PURGE=false UPDATE=false +BUILD_FROM_SOURCE=false # Default install dir (user-writable, no sudo). Overridden by --dir or --system. default_install_dir() { @@ -36,6 +37,7 @@ usage() { echo " --list List available releases (tag, name, published_at) and exit." echo " --version TAG Install from release TAG (e.g. v1.0.0). Default: latest." echo " --update Reinstall latest into the default user directory (~/.local/share/dnsmasq-webui)." + echo " --build-from-source Build from source instead of downloading (requires .NET SDK and a git clone; not supported when run via curl | sh). Auto-detects RID for your machine. Use if the prebuilt binary fails to start (e.g. TypeLoadException)." echo " -h, -?, --help Show this help." echo "" echo "Install location:" @@ -67,8 +69,9 @@ usage() { echo " $0 --uninstall --purge # Remove services, symlinks, and default install dir" echo " sudo $0 --uninstall --purge --system # Also remove /opt/dnsmasq-webui" echo " $0 --list # List releases" + echo " $0 --build-from-source # Build locally (if prebuilt binary fails on your system)" echo "" - echo "After install, configure via appsettings.json or Dnsmasq__* environment variables, then run the binary (or dnsmasq-webui if symlink created). If you used --service, enable/start with systemctl." + echo "After install, configure via appsettings.json or Dnsmasq__* environment variables, then run the binary (or dnsmasq-webui if symlink created). If you used --service, enable/start with systemctl. If the app fails to start with TypeLoadException or a glibc error, try --build-from-source (requires .NET SDK)." exit 0 } @@ -113,17 +116,15 @@ detect_arch() { esac } +# Map to portable RIDs only (linux-x64, linux-arm64, linux-musl-*). Ubuntu and other +# glibc distros use linux-x64/linux-arm64; Alpine uses linux-musl-*. Used for downloading +# release assets (CI builds portable RIDs only). detect_rid() { local arch arch="$(detect_arch)" if [ -f /etc/os-release ]; then . /etc/os-release case "${ID:-}" in - ubuntu) - case "${VERSION_ID:-}" in - 24.04) echo "ubuntu.24.04-$arch"; return ;; - 22.04) echo "ubuntu.22.04-$arch"; return ;; - esac ;; alpine) case "$arch" in x64) echo "linux-musl-x64"; return ;; @@ -139,6 +140,18 @@ detect_rid() { esac } +# Print detected environment (OS, arch) for user feedback. +print_env_detection() { + local rid + rid="$1" + if [ -f /etc/os-release ]; then + . /etc/os-release + echo "Detected: ${ID:-unknown} ${VERSION_ID:-}, $(uname -m) -> RID $rid" + else + echo "Detected: $(uname -m) -> RID $rid" + fi +} + # GET release (latest or by tag). Output raw JSON to stdout. fetch_release() { local api_url @@ -270,8 +283,97 @@ do_uninstall() { exit 0 } +# Build from source and install to INSTALL_DIR. Requires .NET SDK and running from a git clone. +do_install_from_source() { + local script_dir repo_root publish_output rid publish_dir + script_dir="$(cd "$(dirname "$0")" && pwd)" + repo_root="$(cd "$script_dir/.." && pwd)" + + if [ ! -f "$repo_root/scripts/publish-self-contained.sh" ]; then + echo "Error: --build-from-source requires a git clone (run from the repo directory). Not found: $repo_root/scripts/publish-self-contained.sh" >&2 + echo "Do not use --build-from-source when installing via 'curl ... | sh'; clone the repo first, then run ./scripts/install.sh --build-from-source" >&2 + exit 1 + fi + if ! command -v dotnet >/dev/null 2>&1; then + echo "Error: --build-from-source requires the .NET SDK. Install from https://dotnet.microsoft.com/download or your distro (e.g. apt install dotnet-sdk-9.0)." >&2 + exit 1 + fi + + if [ "$SYSTEM_INSTALL" = true ]; then + if [ "$(id -u)" -ne 0 ]; then + echo "Error: --system installs to /opt and requires root. Run with sudo: sudo $0 --system --build-from-source" >&2 + exit 1 + fi + INSTALL_DIR="/opt/dnsmasq-webui" + elif [ -z "$INSTALL_DIR" ]; then + INSTALL_DIR="$(default_install_dir)" + fi + + echo "Building from source (auto-detecting RID for your machine)..." + publish_output="$(cd "$repo_root" && ./scripts/publish-self-contained.sh 2>&1)" || exit $? + echo "$publish_output" + rid="$(echo "$publish_output" | sed -n 's/^Detected RID: \([^ ]*\).*/\1/p')" + if [ -z "$rid" ]; then + rid="$(echo "$publish_output" | sed -n 's/^Publishing self-contained.* for \([^.]*\)\.\.\./\1/p')" + fi + if [ -z "$rid" ]; then + echo "Error: Could not determine RID from build output." >&2 + exit 1 + fi + publish_dir="$repo_root/src/DnsmasqWebUI/bin/Release/net9.0/$rid/publish" + if [ ! -d "$publish_dir" ] || [ ! -f "$publish_dir/DnsmasqWebUI" ]; then + echo "Error: Build output not found at $publish_dir" >&2 + exit 1 + fi + + echo "Installing to $INSTALL_DIR ..." + mkdir -p "$INSTALL_DIR" + cp -a "$publish_dir"/* "$INSTALL_DIR/" + ln -sf DnsmasqWebUI "$INSTALL_DIR/dnsmasq-webui" 2>/dev/null || true + + if [ "$SYSTEM_INSTALL" = true ]; then + ln -sf "$INSTALL_DIR/dnsmasq-webui" /usr/local/bin/dnsmasq-webui 2>/dev/null || true + echo "" + echo "Installed to $INSTALL_DIR (system-wide)" + echo "Run: dnsmasq-webui (or $INSTALL_DIR/dnsmasq-webui)" + else + LOCAL_BIN="${HOME:-}/.local/bin" + if [ -n "${HOME:-}" ] && [ -d "$(dirname "$LOCAL_BIN")" ]; then + mkdir -p "$LOCAL_BIN" + if [ -w "$LOCAL_BIN" ]; then + ln -sf "$INSTALL_DIR/dnsmasq-webui" "$LOCAL_BIN/dnsmasq-webui" 2>/dev/null && echo "Symlink: $LOCAL_BIN/dnsmasq-webui -> $INSTALL_DIR/dnsmasq-webui" || true + fi + fi + echo "" + echo "Installed to $INSTALL_DIR" + echo "Run: $INSTALL_DIR/dnsmasq-webui" + if [ -f "$LOCAL_BIN/dnsmasq-webui" ] 2>/dev/null; then + echo " or: dnsmasq-webui (if ~/.local/bin is in your PATH)" + fi + fi + + if [ "$SERVICE" = true ]; then + if [ "$SYSTEM_INSTALL" = true ] && [ "$(id -u)" -ne 0 ]; then + echo "Error: Installing a system service (--service with --system) requires root." >&2 + exit 1 + fi + if ! command -v systemctl >/dev/null 2>&1; then + echo "Error: systemd not found. --service is only supported on systemd-based systems." >&2 + exit 1 + fi + install_systemd_unit "$INSTALL_DIR" + fi + + echo "Configure via appsettings.json in that directory or Dnsmasq__* environment variables (e.g. Dnsmasq__MainConfigPath=/etc/dnsmasq.conf)." +} + # Main install: fetch release, find asset for RID, download, extract, optionally symlink. do_install() { + if [ "$BUILD_FROM_SOURCE" = true ]; then + do_install_from_source + return + fi + check_jq detect_repo @@ -297,7 +399,7 @@ do_install() { fi rid="$(detect_rid)" - echo "Detected RID: $rid" + print_env_detection "$rid" echo "Fetching release..." release_json="$(fetch_release)" tag="$(echo "$release_json" | jq -r '.tag_name')" @@ -317,24 +419,25 @@ do_install() { echo "Extracting to $INSTALL_DIR ..." unzip -o -q "$tmpzip" -d "$INSTALL_DIR" rm -f "$tmpzip" + ln -sf DnsmasqWebUI "$INSTALL_DIR/dnsmasq-webui" 2>/dev/null || true if [ "$SYSTEM_INSTALL" = true ]; then - ln -sf "$INSTALL_DIR/DnsmasqWebUI" /usr/local/bin/dnsmasq-webui 2>/dev/null || true + ln -sf "$INSTALL_DIR/dnsmasq-webui" /usr/local/bin/dnsmasq-webui 2>/dev/null || true echo "" echo "Installed to $INSTALL_DIR (system-wide)" - echo "Run: dnsmasq-webui (or $INSTALL_DIR/DnsmasqWebUI)" + echo "Run: dnsmasq-webui (or $INSTALL_DIR/dnsmasq-webui)" else # User install: create ~/.local/bin symlink so `dnsmasq-webui` works if ~/.local/bin is in PATH LOCAL_BIN="${HOME:-}/.local/bin" if [ -n "${HOME:-}" ] && [ -d "$(dirname "$LOCAL_BIN")" ]; then mkdir -p "$LOCAL_BIN" if [ -w "$LOCAL_BIN" ]; then - ln -sf "$INSTALL_DIR/DnsmasqWebUI" "$LOCAL_BIN/dnsmasq-webui" 2>/dev/null && echo "Symlink: $LOCAL_BIN/dnsmasq-webui -> $INSTALL_DIR/DnsmasqWebUI" || true + ln -sf "$INSTALL_DIR/dnsmasq-webui" "$LOCAL_BIN/dnsmasq-webui" 2>/dev/null && echo "Symlink: $LOCAL_BIN/dnsmasq-webui -> $INSTALL_DIR/dnsmasq-webui" || true fi fi echo "" echo "Installed to $INSTALL_DIR" - echo "Run: $INSTALL_DIR/DnsmasqWebUI" + echo "Run: $INSTALL_DIR/dnsmasq-webui" if [ -f "$LOCAL_BIN/dnsmasq-webui" ] 2>/dev/null; then echo " or: dnsmasq-webui (if ~/.local/bin is in your PATH)" fi @@ -345,17 +448,21 @@ do_install() { fi echo "Configure via appsettings.json in that directory or Dnsmasq__* environment variables (e.g. Dnsmasq__MainConfigPath=/etc/dnsmasq.conf)." + echo "If the app fails to start with TypeLoadException or a glibc error, try: $0 --build-from-source (requires .NET SDK)." } # Install systemd unit. Call only when SERVICE=true and systemctl exists. # Removes the other service type first (user vs system) so switching works cleanly. -# $1 = INSTALL_DIR (where DnsmasqWebUI binary and appsettings.json live) +# $1 = INSTALL_DIR (contains dnsmasq-webui runnable and appsettings.json) install_systemd_unit() { local dir bin dir="$1" - bin="$dir/DnsmasqWebUI" - if [ ! -f "$bin" ]; then - echo "Warning: Binary $bin not found; skipping systemd unit install." >&2 + if [ -f "$dir/dnsmasq-webui" ]; then + bin="$dir/dnsmasq-webui" + elif [ -f "$dir/DnsmasqWebUI" ]; then + bin="$dir/DnsmasqWebUI" + else + echo "Warning: No binary found in $dir; skipping systemd unit install." >&2 return 0 fi if [ "$SYSTEM_INSTALL" = true ]; then @@ -470,6 +577,10 @@ while [ $# -gt 0 ]; do PURGE=true shift ;; + --build-from-source) + BUILD_FROM_SOURCE=true + shift + ;; -*) echo "Error: unknown option $1" >&2 usage >&2 @@ -488,16 +599,26 @@ if [ "$PURGE" = true ] && [ "$UNINSTALL" != true ]; then echo "Error: --purge must be used with --uninstall. E.g. $0 --uninstall --purge" >&2 exit 1 fi -if [ "$UNINSTALL" = true ]; then - if [ "$UPDATE" = true ] || [ -n "$VERSION" ] || [ "$SERVICE" = true ]; then - echo "Error: --uninstall cannot be combined with install/update options (--version, --update, --service). Run uninstall alone, then install if needed." >&2 - exit 1 + if [ "$UNINSTALL" = true ]; then + if [ "$UPDATE" = true ] || [ -n "$VERSION" ] || [ "$SERVICE" = true ] || [ "$BUILD_FROM_SOURCE" = true ]; then + echo "Error: --uninstall cannot be combined with install/update options (--version, --update, --service, --build-from-source). Run uninstall alone, then install if needed." >&2 + exit 1 + fi + if [ "$LIST" = true ]; then + echo "Error: --uninstall cannot be combined with --list." >&2 + exit 1 + fi fi - if [ "$LIST" = true ]; then - echo "Error: --uninstall cannot be combined with --list." >&2 - exit 1 + if [ "$BUILD_FROM_SOURCE" = true ]; then + if [ "$LIST" = true ]; then + echo "Error: --build-from-source cannot be combined with --list." >&2 + exit 1 + fi + if [ -n "$VERSION" ] || [ "$UPDATE" = true ]; then + echo "Error: --build-from-source builds from current source; do not use --version or --update." >&2 + exit 1 + fi fi -fi if [ "$LIST" = true ]; then if [ "$UNINSTALL" = true ] || [ "$UPDATE" = true ] || [ "$SERVICE" = true ] || [ -n "$VERSION" ]; then echo "Error: --list lists releases and exits; do not combine with install/uninstall options." >&2 diff --git a/scripts/publish-self-contained.sh b/scripts/publish-self-contained.sh index affd879..3e8eed6 100755 --- a/scripts/publish-self-contained.sh +++ b/scripts/publish-self-contained.sh @@ -3,9 +3,7 @@ # for ASP.NET Core). Copy the publish directory to the target host and run the binary. # # Platform auto-detection: if you do not pass a RID, the script picks one from /etc/os-release -# and uname -m. On Ubuntu 24.04/22.04 we use ubuntu.- so the runtime matches the -# host and avoids TypeLoadException seen with generic linux-x64 on those distros. Else we use -# generic linux-* or linux-musl-* (Alpine). Trimming is off by default because PublishTrimmed +# and uname -m. We use generic linux-* (glibc) or linux-musl-* (Alpine). Trimming is off by default because PublishTrimmed # can cause Blazor routing/404 issues. The script cleans the target RID before publish # so previous artefacts (e.g. from a different --trim or RID) cannot pollute the build. set -e @@ -14,6 +12,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT="$REPO_ROOT/src/DnsmasqWebUI/DnsmasqWebUI.csproj" +# Portable RIDs (built in CI). Ubuntu RIDs for local builds only (CI does not build these; +# use when .NET from distro repo so the binary runs on this machine without TypeLoadException). VALID_RIDS="linux-x64 linux-arm64 linux-arm linux-musl-x64 linux-musl-arm64 ubuntu.24.04-x64 ubuntu.24.04-arm64 ubuntu.22.04-x64 ubuntu.22.04-arm64" TRIM=false CLEAN=true @@ -30,7 +30,9 @@ detect_arch() { esac } -# Pick RID from OS and arch so runtime matches host (avoids TypeLoadException on Ubuntu). +# Pick RID from OS and arch. We only add explicit distro RIDs where we've seen +# TypeLoadException with portable linux-x64 (Ubuntu when .NET from distro). Alpine +# uses musl. Other distros (Debian, Fedora, etc.) fall through to linux-x64/linux-arm64. default_rid() { local arch arch="$(detect_arch)" @@ -64,7 +66,7 @@ while [ $# -gt 0 ]; do echo "" echo "Build a self-contained folder publish for Linux. If RID is omitted, the script" echo "auto-detects from the current OS and architecture (recommended on Ubuntu and Alpine)." - echo "Pass options first, then RID if desired (e.g. $0 --trim ubuntu.24.04-x64)." + echo "Pass options first, then RID if desired (e.g. $0 --trim linux-x64)." echo "" echo "Options:" echo " --trim Enable trimming (smaller output; can cause 404/routing issues with Blazor)" @@ -79,7 +81,7 @@ while [ $# -gt 0 ]; do echo " Alpine (musl):" echo " linux-musl-x64 Alpine amd64" echo " linux-musl-arm64 Alpine aarch64" - echo " Ubuntu (use on Ubuntu to avoid TypeLoadException with generic RID):" + echo " Ubuntu (local builds only; not in CI releases; use if prebuilt linux-x64 fails):" echo " ubuntu.24.04-x64 ubuntu.24.04-arm64" echo " ubuntu.22.04-x64 ubuntu.22.04-arm64" echo "" @@ -87,11 +89,11 @@ while [ $# -gt 0 ]; do echo "" echo "Examples:" echo " $0 # Publish for current machine (auto-detect RID)" - echo " $0 ubuntu.24.04-x64 # Publish for Ubuntu 24.04 amd64" + echo " $0 linux-x64 # Publish for glibc amd64 (Debian, Ubuntu, etc.)" echo " $0 linux-arm64 # Publish for Raspberry Pi 4/5 or other aarch64" echo " $0 linux-musl-x64 # Publish for Alpine (e.g. Docker)" echo " $0 --trim linux-x64 # Smaller build (not recommended for Blazor)" - echo " $0 --no-clean ubuntu.24.04-x64 # Skip clean (faster; same RID/options as last run)" + echo " $0 --no-clean linux-x64 # Skip clean (faster; same RID/options as last run)" exit 0 ;; --trim) diff --git a/src/DnsmasqWebUI/Components/App.razor b/src/DnsmasqWebUI/Components/App.razor index b529541..4e0e089 100644 --- a/src/DnsmasqWebUI/Components/App.razor +++ b/src/DnsmasqWebUI/Components/App.razor @@ -6,11 +6,10 @@ - - - - - + + + + diff --git a/src/DnsmasqWebUI/Components/Layout/NavMenu.razor b/src/DnsmasqWebUI/Components/Layout/NavMenu.razor index d315eba..16b075d 100644 --- a/src/DnsmasqWebUI/Components/Layout/NavMenu.razor +++ b/src/DnsmasqWebUI/Components/Layout/NavMenu.razor @@ -4,14 +4,13 @@ - -