diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 5552b86e..b667e673 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 @@ -563,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 '