From b1a75c637c51692abc708f93dd18277475b37ee6 Mon Sep 17 00:00:00 2001 From: R0ck Date: Fri, 4 Sep 2026 07:37:48 +0100 Subject: [PATCH 1/2] package: give the Windows job a locale, which WiX needs to see a path WiX refused a plainly relative directory name: meshbench.wxs(135) : error WIX0389: The Directory/@Name attribute's value, 'MeshBench', is not a relative path. 'MeshBench' is relative by any reading, so this is WiX's own path handling rather than the authoring, and the line above it in the same log says what is wrong with the machine: "icotool: cannot set locale: No such file or directory". Two unrelated tools, one missing locale. It is a known shape on Linux - wixtoolset/issues#7154, Ubuntu 22.04, closed "not planned" - and the reports cluster in minimal environments, that issue and the Docker SDK images beside it, which is what a runner with no locale is. WiX only claims to support Windows at all, so none of this is a supported configuration. C.UTF-8 rather than a generated en_GB: glibc has carried it built in since 2.35, which is this runner's floor, so nothing has to be installed for it to exist. Why this is only appearing now: v0.0.5's installer was built by wixl, which had no such trouble. WiX arrived with the dialogs, because wixl builds none, and the Windows job has failed before reaching the MSI on every run since - the dotnet install, then the pins. This is the first time WiX has actually run here, so it is the first time this could have been seen. Found by a dispatched build rather than by a fourth failed tag. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Q9HbD44EKWWTRYgxbFGxf6 --- .github/workflows/package.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 5552b86e..a4bba6c1 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -533,6 +533,30 @@ jobs: # No fork guard here: this workflow only triggers on tag pushes and # workflow_dispatch, both of which need write access to this repo. runs-on: [self-hosted, linux, x64, lab-2204] + # This runner sets no locale, and two tools in this job read one. icotool + # says so outright - "icotool: cannot set locale: No such file or + # directory" - and WiX then refuses a plainly relative Directory/@Name: + # + # meshbench.wxs(135) : error WIX0389: The Directory/@Name attribute's + # value, 'MeshBench', is not a relative path. + # + # 'MeshBench' is relative by any reading, so that is WiX's own path + # handling and not the authoring. It is a known shape on Linux + # (wixtoolset/issues#7154, Ubuntu 22.04, closed "not planned") and it turns + # up in minimal environments - that issue, and the Docker SDK images in the + # discussion beside it - which is what a runner with no locale is. + # + # C.UTF-8 rather than a generated en_GB: glibc has carried it built in + # since 2.35, which is exactly this runner's floor, so nothing has to be + # installed for it to exist. + # + # v0.0.5's installer was built by wixl, which had no such trouble. WiX + # arrived with the dialogs, wixl building none, and the Windows job has + # failed before reaching the MSI on every run since - so this is the first + # time WiX has actually run here. + env: + LANG: C.UTF-8 + LC_ALL: C.UTF-8 steps: - uses: actions/checkout@v7 - uses: actions/setup-go@v7 From f7f79d26e871cfd61d56a7f60d2f1ef6e00743f3 Mon Sep 17 00:00:00 2001 From: R0ck Date: Fri, 4 Sep 2026 08:43:41 +0100 Subject: [PATCH 2/2] package: build the Windows installer with wixl again WiX cannot build an MSI on Linux. Not "has a bug on Linux": it needs Windows' own msi.dll to write the database, and its first line of output has been saying so all along - "The WiX Toolset only supports Windows. All behavior after this point is undefined." Proven rather than argued, by running the toolset here: - every Directory/@Name is refused as "not a relative path", whatever the name. MeshBench, meshbench, Mesh, mb, A, abc - all WIX0389, on a machine with a perfectly good locale, which is what rules out the locale this branch started as a fix for - remove the Name and it compiles, then dies on DllNotFoundException: Unable to load shared library 'msi.dll' - WiX 6.0.2 does the same. WiX 7 refuses to run at all without accepting the Open Source Maintenance Fee EULA, which is not a dependency to take on in passing So the toolset moved for the dialogs and could never have built here. v0.0.5's installer was built by wixl, which is a native Linux implementation and needs none of that; it is still installed by this job, as part of msitools. Restored from v0.0.5: meshbench.wxs, windows-msi.sh and verify-msi.sh, which had grown assertions for the dialogs and would now refuse a correct package. The WiX setup steps are gone with them - they were the source of two of tonight's faults on their own. What this costs is the install-folder dialog, which has never shipped: 0.0.6 is not out, so nothing released regresses, and the installer is what v0.0.5's was plus everything else in this release. The switches still do the whole job and docs/install.md now says so outright rather than describing a dialog that will not appear. The locale stays. It fixed a real complaint from icotool on the same runner - "cannot set locale: No such file or directory" - which was true, just not what WiX was objecting to. Building the installer on a Windows runner is what the dialogs would cost, and that is a change to the shape of the pipeline rather than to any of these files. Left for 0.0.7. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Q9HbD44EKWWTRYgxbFGxf6 --- .github/workflows/package.yml | 33 ---- CHANGELOG.md | 7 - docs/install.md | 23 ++- packaging/meshbench.wxs | 290 +++++++++++++--------------------- packaging/verify-msi.sh | 32 ---- packaging/windows-msi.sh | 127 +++++++-------- 6 files changed, 187 insertions(+), 325 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index a4bba6c1..b667e673 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -587,39 +587,6 @@ jobs: sudo apt-get install -y --no-install-recommends \ gcc-mingw-w64-x86-64 zip msitools icoutils python3-pil - # The WiX toolset, which is what builds the installer now. A .NET tool, - # so it runs on this Linux runner and the "no Windows runner" property - # this job has always had still holds. - # The action installs to /usr/share/dotnet by default, which the runner - # user cannot create: "Failed to install dotnet, exit code: 1. mkdir: - # cannot create directory '/usr/share/dotnet': Permission denied". On a - # hosted runner that directory is already there and writable, so this - # only ever surfaces on the lab pool - which is the only pool that builds - # releases, so it surfaced on a tag. - # - # $HOME rather than runner.temp or runner.tool_cache: the home directory - # is certainly writable and certainly persists, so the SDK is downloaded - # once rather than once per release, and neither property is a guess - # about how this runner was built. - - name: Where the SDK may be installed - run: echo "DOTNET_INSTALL_DIR=$HOME/.dotnet" >> "$GITHUB_ENV" - - - uses: actions/setup-dotnet@v4 - with: { dotnet-version: '8.0' } - - # Idempotent on purpose. These are persistent self-hosted runners, so the - # second release to build here would meet "tool 'wix' is already - # installed" and fail a step that had nothing wrong with it. - - name: WiX - run: | - set -e - dotnet tool install --global wix --version 5.0.2 || - dotnet tool update --global wix --version 5.0.2 - echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH" - export PATH="$HOME/.dotnet/tools:$PATH" - wix extension list -g | grep -q WixToolset.UI.wixext || - wix extension add -g WixToolset.UI.wixext/5.0.2 - wix --version - name: Build env: diff --git a/CHANGELOG.md b/CHANGELOG.md index f14d6706..5f36116f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,13 +59,6 @@ is in it. an elevated process. All three are dealt with, the last by looking in the same places on both sides of the search so no link is needed. -- **The Windows installer asks where to go, and says when it has finished.** It - had no dialogs at all, because the tool that built it builds none, so every - answer had to be an `msiexec` switch and a double-clicked `.msi` put itself - in Program Files without a word. It now offers a folder, reports the location - to Apps and Features, keeps it across an upgrade, and carries the MeshBench - card. The switches all still work. - ### Changed - **The emulators hold the SX1262 themselves.** An emulated node used to run in diff --git a/docs/install.md b/docs/install.md index cb5b8c4a..0dfd39ab 100644 --- a/docs/install.md +++ b/docs/install.md @@ -94,14 +94,21 @@ Intel Macs are not built yet. Ask if you need one. Two downloads, and either is a complete build. -`meshbench-windows-x86_64-bundled.msi` is the installer. Double-click it and it asks -where to go — `C:\Program Files\MeshBench` unless you browse somewhere else — -adds a Start menu entry, and appears in Apps and Features with an uninstall and -the location it used. Installing a newer one replaces the installation rather -than putting a second copy beside it, and keeps the directory you chose. The -uninstall removes what it installed and nothing else: your fixtures, cached -firmware, terrain tiles and settings live under your profile and are left -alone. +`meshbench-windows-x86_64-bundled.msi` is the installer. Double-click it and it +installs to `C:\Program Files\MeshBench`, adds a Start menu entry, and appears +in Apps and Features with an uninstall. Installing a newer one replaces the +installation rather than putting a second copy beside it. The uninstall removes +what it installed and nothing else: your fixtures, cached firmware, terrain +tiles and settings live under your profile and are left alone. + +It asks nothing while it runs. The toolset that builds it cannot draw dialogs, +and the one that can only runs on Windows, which this pipeline has none of. To +install somewhere else, or per-user, pass it on the command line: + +``` +msiexec /i meshbench-windows-x86_64-bundled.msi INSTALLDIR="D:\Tools\MeshBench" +msiexec /i meshbench-windows-x86_64-bundled.msi MSIINSTALLPERUSER=1 +``` Everything the wizard offers can also be a switch, which is what you want for an unattended install, and there are two it does not offer. From a Command diff --git a/packaging/meshbench.wxs b/packaging/meshbench.wxs index bcd4f322..5838c925 100644 --- a/packaging/meshbench.wxs +++ b/packaging/meshbench.wxs @@ -1,44 +1,56 @@ - - + + Version="$(var.Version)" + Manufacturer="MeshBench" + UpgradeCode="6f2a1c84-7f3b-4c9e-9a6d-0d8f5b1e2c30"> - + + - + - + + + + - - - + + + - - - + + NOT NEWERVERSIONDETECTED OR Installed + - - - + - - - - + + - - - + + + + + + + + + + + - - + + - - - + - - + + - - + - - + + - - - - - + - + - - - - - - - - - - - - + diff --git a/packaging/verify-msi.sh b/packaging/verify-msi.sh index 14fea554..54cc1950 100755 --- a/packaging/verify-msi.sh +++ b/packaging/verify-msi.sh @@ -92,38 +92,6 @@ case "$(prop SecureCustomProperties)" in esac say "installs under Program Files, per machine or per user, into a chosen location" -# The location it chose, said back. Nothing sets ARPINSTALLLOCATION by itself, -# and without it Apps and Features shows a blank Install location - so a person -# who wants to know where their copy went has to guess. The action is generated -# from the SetProperty in meshbench.wxs and named after the property it sets. -holds "$(table CustomAction)" SetARPINSTALLLOCATION || - bad "$msi does not set ARPINSTALLLOCATION, so Apps and Features would show no install location" -holds "$(table InstallExecuteSequence)" SetARPINSTALLLOCATION || - bad "$msi never runs SetARPINSTALLLOCATION, so the property is authored and unset" -say "reports its install location to Apps and Features" - -# The dialogs. WiX builds them and wixl could not, which is the whole reason -# this package moved toolset - so a build that quietly lost them would undo it. -dlgs=$(table Dialog) -for d in WelcomeDlg InstallDirDlg BrowseDlg ExitDialog; do - holds "$dlgs" "$d" || bad "$msi has no $d, so the installer cannot ask or report" -done -say "asks where to install, and says when it has finished" - -# And where an upgrade goes. Without the search, a version installed into a -# chosen directory comes back in Program Files and the choice is silently -# undone; measured before it was added. -holds "$(table Registry)" InstallDir || - bad "$msi does not record its install directory, so an upgrade would not stay put" -say "remembers where it was installed" - -# And that it can be taken off again. An MSI gets this for nothing, which is -# exactly why it is worth asserting: the maintenance dialogs come from the UI -# extension, and a package that lost them would still install perfectly and -# only be found wanting by somebody trying to remove it. -holds "$dlgs" MaintenanceTypeDlg || - bad "$msi has no maintenance dialog, so running it again offers no Remove" -say "offers Repair and Remove when run again" holds "$(table Shortcut)" ProgramMenuFolder || bad "$msi makes no Start menu entry" say "Start menu entry present" diff --git a/packaging/windows-msi.sh b/packaging/windows-msi.sh index 0e37fc35..abdca086 100755 --- a/packaging/windows-msi.sh +++ b/packaging/windows-msi.sh @@ -5,22 +5,16 @@ # # is the directory meshbench.exe sits in, the same one the zip is # made from, and everything in it goes into the installer. That is the point: -# the emulators and the chip model are found beside the binary, so an +# the emulators and the SX1262 model are found beside the binary, so an # installer that carried the binary alone would produce a build that cannot # emulate a board and cannot say why. # -# No Windows runner. The WiX toolset is a .NET tool and runs here, beside the -# mingw cross-build that produced the .exe. -# -# It was wixl until the installer was asked to say it had finished and to offer -# a folder to install into. wixl builds no dialogs - GNOME/msitools#3 - so -# neither was possible and every answer had to be a switch on the msiexec -# command line. Those switches still work and are still documented; there is -# now also a wizard for the two that people expect to click. -# -# msitools has not gone: msiinfo reads the built package for verify-msi.sh, and -# msibuild puts back the one row WiX will not author. See the ALLUSERS note -# further down. +# No Windows runner. wixl reads the WiX source and writes the .msi here, beside +# the mingw cross-build that produced the .exe. What that costs is the +# installer's user interface: wixl builds no dialogs, so msiexec shows a +# progress bar and takes its answers from the command line instead. Those +# answers - a location, per-user, a desktop shortcut - are written out for +# somebody installing this in docs/install.md. set -euo pipefail stage=${1:?the directory meshbench.exe sits in} @@ -30,32 +24,16 @@ out=${3:?the .msi to write} here=$(cd "$(dirname "$0")" && pwd) missing="" -for t in wix msiinfo msibuild icotool python3; do +for t in wixl wixl-heat msiinfo msibuild icotool python3; do command -v "$t" >/dev/null || missing="$missing $t" done if [ -n "$missing" ]; then - echo "::error::windows-msi: missing:$missing - wix is the WiX toolset," \ - "installed with 'dotnet tool install --global wix'; msiinfo and" \ - "msibuild are 'msitools'; icotool is 'icoutils'" >&2 + echo "::error::windows-msi: missing:$missing - wixl and wixl-heat are the" \ + "'wixl' package on Debian and Ubuntu rather than 'msitools', which is" \ + "where msiinfo lives; icotool is 'icoutils'" >&2 exit 1 fi -# The dialog bitmaps are composed with Pillow, which python3 does not carry by -# itself. Checked here rather than met as a traceback three minutes into a -# release build. -python3 -c 'import PIL' 2>/dev/null || { - echo "::error::windows-msi: python3 cannot import PIL - install python3-pil" >&2 - exit 1 -} - -# The dialogs come from an extension, and a missing one fails inside the build -# with a message about an unknown element rather than about a missing package. -wix extension list -g 2>/dev/null | grep -q WixToolset.UI.wixext || { - echo "::error::windows-msi: the WiX UI extension is not installed: run" \ - "'wix extension add -g WixToolset.UI.wixext'" >&2 - exit 1 -} - # Windows Installer compares three numeric fields and ignores anything after # them, so a development version has no version to compare and is given the # lowest one. Every real release is a plain X.Y.Z and passes through. @@ -73,16 +51,15 @@ import uuid, sys print(uuid.uuid5(uuid.UUID('$upgrade'), sys.argv[1]).urn[9:].upper()) " "$msiversion") -# A scratch directory for the two things built rather than harvested: the icon -# and the dialog bitmaps. Absolute paths are handed to the build, which wixl -# could not take - it fed them to g_file_get_child, which rejected them with a -# GLib assertion and then said it could not find the file, which is why this -# used to symlink both source trees into one directory and name them -# relatively. +# wixl reads every File Source relative to the directory it is run from, and +# hands an absolute one to g_file_get_child, which rejects it with a GLib +# assertion and then says it cannot find the file. So the two source trees are +# linked into one working directory and named relatively from there. work=$(mktemp -d) trap 'rm -rf "$work"' EXIT extra=$work/extra mkdir -p "$extra" +ln -s "$(cd "$stage" && pwd)" "$work/stage" out=$(cd "$(dirname "$out")" && pwd)/$(basename "$out") # Windows wants one icon file holding every size, the way macOS wants one @@ -94,40 +71,56 @@ icotool -c -o "$extra/meshbench.ico" \ "$here/icons/meshbench-48.png" "$here/icons/meshbench-64.png" \ "$here/icons/meshbench-128.png" "$here/icons/meshbench-256.png" -# The two bitmaps the dialogs are drawn on. See packaging/dialog-bitmaps.py for -# why they are shaped the way they are, and why this is Pillow rather than the -# one-line ImageMagick it started as. -python3 "$here/dialog-bitmaps.py" \ - "$here/../docs/brand/meshbench-card.png" \ - "$here/icons/meshbench-256.png" \ - "$extra" - sed "s//$version/" "$here/installed-by-msi.txt" \ > "$extra/installed-by-msi.txt" -files=$(find "$stage" -type f | wc -l) +# Everything in the bundle, as components. Sorted, so the source that goes +# into a build is the same from one run to the next and a diff of two builds +# is about what changed rather than about what order find walked in. +(cd "$stage" && find . -type f | sed 's|^\./||' | LC_ALL=C sort) | + wixl-heat --var var.Stage --directory-ref INSTALLDIR \ + --component-group Bundle --prefix "" --win64 > "$work/bundle.wxs" + +files=$(grep -c '