Skip to content

Add Arch packages and proper Windows installers to the packaging pipeline - #2118

Open
nullPointerEnjoyer wants to merge 1 commit into
masterfrom
feature/arch-windows-packaging
Open

nullPointerEnjoyer wants to merge 1 commit into
masterfrom
feature/arch-windows-packaging

Conversation

@nullPointerEnjoyer

Copy link
Copy Markdown

Summary

Extends the native packaging pipeline (deb/rpm from #2115) with:

Arch Linux (.pkg.tar.zst)packaging/arch/

  • Builder runs in a pinned archlinux:base container; makepkg (as a dedicated build user) repackages the prebuilt release binaries from PKGBUILD templates; install scriptlet applies the preset policy (Arch convention: enable, don't start)
  • Dependencies resolved from the binaries via pacman -F on the arch-matched x86_64 leg; the aarch64 leg repackages cross-target (Arch publishes no arm64 images) with a static fallback map, unstripped binaries and stub man pages — no qemu needed
  • namcap gate + fresh-container install smoke tests (arm64 skips the install step: pacman refuses foreign-arch packages)
  • Artifacts: Mintlayer_Node{,_GUI}_linux_<version>_<arch>.pkg.tar.zstrelease.yml's Mintlayer*/* glob picks them up unchanged
  • Not published to the AUR (registration is currently paused); users install with pacman -U directly (documented in packaging/README.md)

Windows (NSIS)build-tools/win/

  • Replaces the GUI-only create-nsis-script.ps1 with renderable templates (nsi/*.nsi.in + shared common.nsh macros) and a generator producing two installers:
    • Mintlayer_Node_win_<v>_Setup.exe (new): all CLI tools, optional PATH entry, optional mainnet service (sc.exe) + TCP firewall rule
    • GUI setup: all-users shortcuts (SetShellVarContext all), upgrade handling, and a fix for the broken ${SMPROGRAMS} constant carried over from the old script (Start Menu shortcuts never worked)
  • smoke-install.ps1: silent install → verify files/registry/PATH/--help → silent uninstall → verify clean, on the CI runner
  • release_windows.yml: gains workflow_dispatch dry-run + git-describe version fallback (parity with the Linux workflow)

Shared helperspackaging/common/lib.sh + packaging/images.env

  • 7-binary list, version validation and man-page generation single-sourced across the three builders and smoke tests
  • Image pins single-sourced (also fixes the fedora:latest vs fedora:44 drift between test-local.sh and CI)

Drive-by fixes (found by the review agents + local testing)

  • Version charset check rejected any suffixed version (1.4.1-rc1) in deb/rpm/arch due to a bash glob range quirk (+-a parsed as a range)
  • namcap gate keyed on its E: output (namcap always exits 0, even on errors)
  • test-local.sh: run_step/summary block was only defined in the --skip-build branch — default (building) runs could never reach it
  • NSIS PATH rewrite: length-gated (stock NSIS strings cap at 1024 chars — a long machine PATH would be silently truncated), previous value backed up, no ;; residue on removal
  • Checkout hardening: persist-credentials: false + explicit read-only permissions on the packaging jobs

Testing

Local end-to-end (docker, real 1.4.x binaries from the deb-container build):

  • all six artifacts build + lint (lintian/rpmlint/namcap) + install-smoke + pass the artifact-name gate
  • Arch x86_64 native and aarch64 cross-target legs; both installers compile via the actual PowerShell generator + makensis; shellcheck/PowerShell-parse/YAML clean

CI plan: workflow_dispatch dry-runs of both release workflows on this branch will exercise the real matrix (arm64 binaries, Windows silent install) before the 1.4.1 tag.

…line

Arch Linux (.pkg.tar.zst), mirroring the deb/rpm builders:
- packaging/arch/build.sh runs in a pinned archlinux:base container; makepkg
  as a dedicated build user repackages the prebuilt release binaries
  (PKGBUILD templates, install scriptlet applies the preset policy)
- dependencies resolved from the binaries via pacman -F on the arch-matched
  x86_64 leg; the aarch64 leg repackages cross-target (Arch publishes no
  arm64 images) with a static fallback map, unstripped binaries and stub man
  pages
- namcap gate (keyed on its ' E: ' output: namcap always exits 0) and
  fresh-container install smoke tests; arm64 skips the install smoke since
  pacman refuses foreign-arch packages
- release_linux.yml builds + uploads both arches; test-local.sh replicates

Windows (NSIS), replacing the GUI-only setup.exe:
- renderable templates (build-tools/win/nsi/*.nsi.in + common.nsh macros)
  and create-nsis-installers.ps1 produce two installers: a new node Setup
  (all CLI tools, optional PATH entry with length-gated registry rewrite and
  backup, optional mainnet service + TCP firewall rule) and an improved GUI
  setup (all-users shortcuts, upgrade handling, fixed the broken
  \${SMPROGRAMS} constant carried over from the old script)
- smoke-install.ps1 silent-installs, verifies files/registry/PATH/--help and
  uninstalls on the CI runner; release_windows.yml gains a workflow_dispatch
  dry run and version fallback like the Linux workflow

Shared helpers (packaging/common/lib.sh): the 7-binary list, version
validation (the deb/rpm charset checks rejected any suffixed version - e.g.
1.4.1-rc1 - due to a glob range quirk) and man-page generation, now
single-sourced across the three builders and smoke tests; container image
pins moved to packaging/images.env (also fixes the fedora:latest drift in
test-local.sh); test-local.sh run_step/summary block moved out of the
--skip-build branch where a default (building) run could never reach it.

Checkout hardening: persist-credentials: false and read-only permissions on
the packaging jobs.
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown

🔍 OpenCodeReview found 18 issue(s) in this PR.

  • ✅ Successfully posted inline: 7 comment(s)
  • 📋 Routed to summary by policy: 11 comment(s)

bug · low

📄 build-tools/win/create-nsis-installers.ps1 (L73-L73)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category bug)

The script writes the rendered .nsi (which embeds @NSI_DIR@, i.e. $TemplateDir = $PSScriptRoot\nsi) as strict ASCII. If the checkout path contains non-ASCII characters (localized usernames, non-ASCII CI workspace paths), [System.Text.Encoding]::ASCII silently substitutes '?', producing a broken !addincludedir path. Consider validating that $TemplateDir is ASCII-representable, or write with UTF-8 without BOM if the makensis version in use supports it.

💡 Suggested Change

Before:

[System.IO.File]::WriteAllText($script, $content, [System.Text.Encoding]::ASCII)

After:

if (($TemplateDir -match '[^\x00-\x7F]') -or ($script -match '[^\x00-\x7F]')) {
    throw "non-ASCII path not supported with ASCII-encoded NSIS scripts: $TemplateDir"
}
[System.IO.File]::WriteAllText($script, $content, [System.Text.Encoding]::ASCII)

maintainability · low

📄 build-tools/win/smoke-install.ps1 (L90-L93)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category maintainability)

$LASTEXITCODE is checked after each '--help' invocation but never reset inside the loop. If a future edit routes the binary through a wrapper that does not set $LASTEXITCODE (e.g. a .cmd shim), a stale value from a previous iteration could validate the wrong binary. Clearing it before each call makes the check self-contained.

💡 Suggested Change

Before:

foreach ($bin in $Binaries) {
    $exe = Join-Path $InstallDir "$bin.exe"
    & $exe --help *> $null
    if ($LASTEXITCODE -ne 0) {

After:

foreach ($bin in $Binaries) {
    $exe = Join-Path $InstallDir "$bin.exe"
    $global:LASTEXITCODE = 0
    & $exe --help *> $null
    if ($LASTEXITCODE -ne 0) {

maintainability · low

📄 .github/workflows/release_windows.yml (L17-L21)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category maintainability)

The Windows release job has no timeout-minutes (the Linux release workflow sets 180). The job now runs two full NSIS builds plus silent install/uninstall smoke tests that spawn installers and poll for up to 30s each; a hung installer or uninstaller would keep the runner occupied indefinitely. Add a job-level timeout-minutes: consistent with release_linux.yml.

💡 Suggested Change

Before:

jobs:
  build:
    runs-on: windows-latest
    permissions:
      contents: read

After:

jobs:
  build:
    runs-on: windows-latest
    timeout-minutes: 180
    permissions:
      contents: read

style · low

📄 packaging/test-local.sh (L102-L103)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category style)

This line was re-indented by one extra space during the block move; the if/else/fi structure is correct, but the indentation is inconsistent with the rest of the file.

💡 Suggested Change

Before:

    for arch in "${ARCHES[@]}"; do
        test -f "${BIN_DIR[$arch]}/node-daemon" ||

After:

    for arch in "${ARCHES[@]}"; do
        test -f "${BIN_DIR[$arch]}/node-daemon" ||

maintainability · low

📄 packaging/common/lib.sh (L68-L70)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category maintainability)

The stub man page printf is duplicated in both branches of gen_man. If the stub format changes, one copy can easily be missed. Consider a small write_stub_man helper (or fall through to the same printf after the warning).

💡 Suggested Change

Before:

            echo "warning: $binname --help not runnable, shipping stub man page" >&2
            printf '.TH %s 1\n.SH NAME\n%s \\- Mintlayer tool\n' "$binname" "$binname" \
                > "$man_dir/$binname.1"

After:

            echo "warning: $binname --help not runnable, shipping stub man page" >&2
            write_stub_man "$man_dir/$binname.1" "$binname"

maintainability · low

📄 packaging/test-local.sh (L29-L30)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category maintainability)

The fallback literal 'archlinux:base-20260913.0.592969' duplicates the pin already defined in images.env (sourced on the line above). If images.env exists, this line is dead; if it is missing/renamed, the script silently continues with a stale copy instead of failing, defeating the 'single source of truth' intent and letting local builds drift from CI. Prefer sourcing with a guard (e.g. [ -f "$PKG_ROOT/images.env" ] || { echo ...; exit 1; }) and dropping the duplicated literal.

💡 Suggested Change

Before:

. "$PKG_ROOT/images.env"
ARCH_IMAGE="${ARCH_IMAGE:-archlinux:base-20260913.0.592969}"

After:

[ -f "$PKG_ROOT/images.env" ] || { echo "images.env not found" >&2; exit 1; }
. "$PKG_ROOT/images.env"

test · low

📄 build-tools/win/smoke-install.ps1 (L70-L72)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category test)

The uninstall verification does not confirm the uninstaller's exit path: it polls only for uninstall.exe deletion but never asserts that the installer's own exit code from Start-Process was meaningful — actually the larger gap is that the GUI install's artifacts beyond binaries are unverified: the desktop and Start Menu shortcuts created with SetShellVarContext all are never checked on install or confirmed removed after uninstall, so a broken shortcut section would pass this smoke test silently.

💡 Suggested Change

Before:

if (-not (Test-Path (Join-Path $InstallDir "uninstall.exe"))) {
    throw "not installed: uninstall.exe"
}

After:

if (-not (Test-Path (Join-Path $InstallDir "uninstall.exe"))) {
    throw "not installed: uninstall.exe"
}
if ($Kind -eq "gui") {
    foreach ($lnk in @(
        (Join-Path ([Environment]::GetFolderPath("CommonDesktopDirectory")) "Mintlayer Node GUI.lnk"),
        (Join-Path $env:ALLUSERSPROFILE "Microsoft\Windows\Start Menu\Programs\Mintlayer\Mintlayer Node GUI.lnk"))) {
        if (-not (Test-Path $lnk)) { throw "not installed: $lnk" }
    }
}

maintainability · low

📄 build-tools/win/create-nsis-installers.ps1 (L61-L62)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category maintainability)

Generated installer scripts are written into $PWD (normally the repo root). This works but leaves build artifacts mixed with sources and depends on the caller's working directory; consider writing them under the template/build output directory or a temp dir (the rendered OutFile is a relative path, so you may need SetOutPath or an absolute OutFile to keep the .exe in $PWD). At minimum, verify .gitignore covers installer-node.nsi / installer-gui.nsi so they are never committed.

💡 Suggested Change

Before:

$template = Join-Path $TemplateDir $TemplateName
$script = Join-Path $PWD $ScriptName

After:

$template = Join-Path $TemplateDir $TemplateName
$script = Join-Path $PWD $ScriptName  # consider $PSScriptRoot\..\build or a temp dir instead

bug · low

📄 packaging/checks/smoke-arch.sh (L10-L12)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category bug)

No argument validation: under set -u, invoking the script without arguments aborts with "unbound variable" instead of a usage message, and an empty $1 reaches readlink. Add a usage guard like the builders have (required PKG_FILE/PKG_NAME/KIND checks), and optionally validate KIND against node|gui like build.sh validates --arch.

💡 Suggested Change

Before:

PKG_FILE="$(readlink -f "$1")"
PKG_NAME="$2"
KIND="$3"

After:

[ $# -eq 3 ] || { echo "usage: $0 <path/to/pkg.pkg.tar.zst> <package-name> node|gui" >&2; exit 2; }
case "$3" in node|gui) ;; *) echo "invalid kind: $3" >&2; exit 2 ;; esac
PKG_FILE="$(readlink -f "$1")"
PKG_NAME="$2"
KIND="$3"

maintainability · low

📄 packaging/arch/build.sh (L194-L197)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category maintainability)

The cross-target GUI package inherits FALLBACK_DEPENDS_NODE wholesale, adding libdbus/libusb/systemd-libs to a GUI binary that almost certainly doesn't need them (the comment says these were derived from the node binaries' ldd output). The base set glibc/gcc-libs is shared, but consider a node-only subset (libdbus/libusb/systemd-libs) so the GUI fallback doesn't pull in unrelated packages on a minimal Arch install.


bug · low

📄 packaging/arch/build.sh (L205-L205)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category bug)

If the dependency resolution yields an empty DEPENDS (e.g. all payloads statically linked so LIBS is empty), pacman -S --noconfirm --needed --asdeps runs with no targets, errors ("no targets specified"), and aborts the build under set -e on a otherwise-valid package. Guard the install with a DEPENDS emptiness check.

💡 Suggested Change

Before:

pacman -S --noconfirm --needed --asdeps $DEPENDS >/dev/null

After:

[ -n "$DEPENDS" ] && pacman -S --noconfirm --needed --asdeps $DEPENDS >/dev/null

Comment on lines +204 to +205
- name: Smoke test Arch packages
if: matrix.arch == 'x86_64'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Unlike the deb and rpm legs (whose smoke tests run under qemu via --platform linux/arm64 for both matrix archs), the Arch arm64 package is built and uploaded without any install/runtime verification: this smoke step is gated to x86_64 and the smoke-arch.sh arm64 leg never runs. A broken aarch64 payload (wrong binary, missing files, bad .PKGINFO) would ship silently. Consider adding a container-free verification (e.g. extract the .pkg.tar.zst with tar and check binaries/paths/.PKGINFO arch, similar to verify-artifacts.sh) for the aarch64 leg instead of relying only on the builder's self-check.

Comment on lines +90 to +91
.\build-tools\win\smoke-install.ps1 -Installer "Mintlayer_Node_win_${{ steps.get_version.outputs.VERSION }}_Setup.exe" -AppName "Mintlayer Node" -Kind node -Version "${{ steps.get_version.outputs.VERSION }}"
.\build-tools\win\smoke-install.ps1 -Installer "Mintlayer_Node_GUI_win_${{ steps.get_version.outputs.VERSION }}_Setup.exe" -AppName "Mintlayer Node GUI" -Kind gui -Version "${{ steps.get_version.outputs.VERSION }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · medium
The extracted VERSION is interpolated directly into this pwsh run: block via ${{ steps.get_version.outputs.VERSION }}. On a non-tag trigger the fallback uses git describe output, and a crafted tag/branch name (or ref) could break out of the quoted string and execute arbitrary code before the regex validation inside the .ps1 scripts ever runs. Pass the value through an env: block and reference $env:VERSION instead.

Suggestion:

Suggested change
.\build-tools\win\smoke-install.ps1 -Installer "Mintlayer_Node_win_${{ steps.get_version.outputs.VERSION }}_Setup.exe" -AppName "Mintlayer Node" -Kind node -Version "${{ steps.get_version.outputs.VERSION }}"
.\build-tools\win\smoke-install.ps1 -Installer "Mintlayer_Node_GUI_win_${{ steps.get_version.outputs.VERSION }}_Setup.exe" -AppName "Mintlayer Node GUI" -Kind gui -Version "${{ steps.get_version.outputs.VERSION }}"
env:
VERSION: ${{ steps.get_version.outputs.VERSION }}
run: |
.\build-tools\win\smoke-install.ps1 -Installer "Mintlayer_Node_win_${VERSION}_Setup.exe" -AppName "Mintlayer Node" -Kind node -Version "${VERSION}"

Comment on lines +22 to +24
if ($Version -notmatch '^[0-9][0-9A-Za-z.~+-]*$') {
throw "invalid version '$Version' (expected digits-first X.Y.Z[-suffix])"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
The Windows version validation is looser than the Linux one in packaging/common/lib.sh (validate_version requires X.Y.Z with an optional -suffix). Here, strings like "1", "1.2" or "1.2.3-rc1-x-y" pass. Since both package families ship from the same tag, keeping a single consistent rule avoids producing a Windows installer filename that the Linux gates would reject (or vice versa). Consider requiring at least two dots, mirroring the X.Y.Z[-suffix] rule.

Suggestion:

Suggested change
if ($Version -notmatch '^[0-9][0-9A-Za-z.~+-]*$') {
throw "invalid version '$Version' (expected digits-first X.Y.Z[-suffix])"
}
if ($Version -notmatch '^[0-9]+\.[0-9]+\.[0-9]+([-+~][0-9A-Za-z.~+-]*)?$') {
throw "invalid version '$Version' (expected X.Y.Z[-suffix])"
}

Comment on lines +29 to +30
$InstallDir = Join-Path $env:ProgramFiles (Join-Path "Mintlayer" $AppName)
$UninstKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Mintlayer $AppName"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
The smoke test assumes a 64-bit PowerShell: it reads $env:ProgramFiles and HKLM:\SOFTWARE...\Uninstall. The templates install to $PROGRAMFILES64 and write the uninstall key in the native registry view, so under 32-bit PowerShell (e.g. Windows PowerShell x86, or a CI step launched from a 32-bit shell) $InstallDir would resolve to 'Program Files (x86)' and Get-ItemProperty would be redirected to WOW6432Node, causing spurious failures. Add an explicit 64-bit check up front (or use $env:ProgramW6432 and the Sysnative path).

Suggestion:

Suggested change
$InstallDir = Join-Path $env:ProgramFiles (Join-Path "Mintlayer" $AppName)
$UninstKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Mintlayer $AppName"
if ([Environment]::Is64BitProcess -ne $true) {
throw "run this script under 64-bit PowerShell (installers use $PROGRAMFILES64 and the native HKLM view)"
}
$InstallDir = Join-Path $env:ProgramFiles (Join-Path "Mintlayer" $AppName)
$UninstKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Mintlayer $AppName"

Comment thread packaging/arch/build.sh
echo "$VERSION_FORMAT_ERROR" >&2
exit 2
fi
PKGVER="$(printf '%s' "$VERSION" | tr -- '-~' '__')"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
validate_version permits '' but not '_', so mapping both '-' and '' to '_' makes distinct versions collide on the same pkgver (e.g. 1.4.1-rc1 and 1.4.1rc1 both become 1.4.1_rc1, which pacman treats as the same package version). The rpm builder maps only '-' to '', which is injective — do the same here.

Suggestion:

Suggested change
PKGVER="$(printf '%s' "$VERSION" | tr -- '-~' '__')"
PKGVER="$(printf '%s' "$VERSION" | tr -- '-' '_')"

Comment thread packaging/arch/build.sh
Comment on lines +146 to +148
for binpath in "$BR"/usr/bin/*; do
file "$binpath" | grep -q "not stripped" && strip --strip-unneeded "$binpath"
done

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
Under set -e, file ... | grep -q "not stripped" && strip ... aborts the whole build if a binary is already stripped (grep returns 1 and the && list's failure propagates). The deb builder deliberately uses an if wrapper for exactly this reason (see deb/build.sh comment about the "already stripped" match failure tripping errexit). Use the same form here.

Suggestion:

Suggested change
for binpath in "$BR"/usr/bin/*; do
file "$binpath" | grep -q "not stripped" && strip --strip-unneeded "$binpath"
done
for binpath in "$BR"/usr/bin/*; do
if file "$binpath" | grep -q "not stripped"; then
strip --strip-unneeded "$binpath"
fi
done

Comment thread packaging/test-local.sh
@@ -71,6 +78,7 @@ fi
echo "pulling container images..."
docker pull -q debian:12 >/dev/null
docker pull -q fedora:latest >/dev/null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
images.env is sourced above and its header claims it is the single source of truth shared with CI, but this script still pulls and runs fedora:latest throughout instead of the pinned $FEDORA_IMAGE. Local rpm builds therefore drift from CI (fedora:44) and the pin provides no reproducibility here. Use "$FEDORA_IMAGE" in the docker pull and all fedora docker run invocations (debian:12 could be similarly pinned or noted as intentionally unpinned).

Suggestion:

Suggested change
docker pull -q fedora:latest >/dev/null
docker pull -q "$FEDORA_IMAGE" >/dev/null

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants