From c2771719633baa862bb98c86a08be426a31b3d97 Mon Sep 17 00:00:00 2001 From: Hein Date: Wed, 1 Jul 2026 18:01:25 +0200 Subject: [PATCH] feat(pkg-nsis): add Windows installer workflow --- .gitea/workflows/release.yml | 46 +++++++ windows/installer.nsi | 226 +++++++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 windows/installer.nsi diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 324208d..ea98e97 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -208,6 +208,52 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + pkg-nsis: + name: Windows installer + needs: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Build NSIS installer + run: | + set -euo pipefail + VERSION="${{ github.event.inputs.tag || github.ref_name }}" + PKGVER="${VERSION#v}" + + sudo apt-get update -y + sudo apt-get install -y nsis + + GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build \ + -trimpath \ + -ldflags "-X main.version=${PKGVER}" \ + -o pgtidy.exe \ + ./cmd/pgtidy + + makensis -DVERSION="${PKGVER}" -DSRC_EXE="$(pwd)/pgtidy.exe" windows/installer.nsi + + - name: Upload to release + run: | + set -euo pipefail + TAG="${{ github.event.inputs.tag || github.ref_name }}" + RELEASE=$(curl -s "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \ + -H "Authorization: token ${GITHUB_TOKEN}") + UPLOAD_URL=$(echo "$RELEASE" | grep -o '"upload_url":"[^"]*"' | cut -d'"' -f4 | sed 's/{[^}]*}//') + [ -z "$UPLOAD_URL" ] && { echo "upload_url not found: $RELEASE"; exit 1; } + for f in windows/pgtidy-setup-*.exe; do + echo "Uploading $(basename "$f")..." + curl -s -X POST "${UPLOAD_URL}?name=$(basename "$f")" \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@${f}" > /dev/null + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + pkg-aur: name: AUR package needs: release diff --git a/windows/installer.nsi b/windows/installer.nsi new file mode 100644 index 0000000..0d9ad46 --- /dev/null +++ b/windows/installer.nsi @@ -0,0 +1,226 @@ +; PgTidy Windows installer +; +; Build with (VERSION must match the release tag, without the leading "v"): +; makensis /DVERSION=1.2.3 /DSRC_EXE=path\to\pgtidy.exe windows\installer.nsi +; +; Installs pgtidy.exe into Program Files, adds the install dir to the +; machine-wide PATH, and on startup checks the git.warky.dev Gitea API for a +; newer release than the one being installed. + +!ifndef VERSION + !define VERSION "0.0.0" +!endif +!ifndef SRC_EXE + !define SRC_EXE "..\dist\pgtidy.exe" +!endif + +!define PRODUCT_NAME "PgTidy" +!define PRODUCT_PUBLISHER "Warky Devs" +!define PRODUCT_HOMEPAGE "https://git.warky.dev/wdevs/pgtidy" +!define RELEASES_API_URL "https://git.warky.dev/api/v1/repos/wdevs/pgtidy/releases/latest" +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\PgTidy" +!define ENV_KEY 'HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"' + +!include "MUI2.nsh" +!include "LogicLib.nsh" + +Name "${PRODUCT_NAME} ${VERSION}" +OutFile "pgtidy-setup-${VERSION}.exe" +InstallDir "$PROGRAMFILES64\PgTidy" +InstallDirRegKey HKLM "${UNINST_KEY}" "InstallLocation" +RequestExecutionLevel admin +Unicode true + +!define MUI_ABORTWARNING +!define MUI_ICON "..\assets\logo_128.ico" +!define MUI_UNICON "..\assets\logo_128.ico" + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_LICENSE "..\LICENSE" +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "English" + +; --------------------------------------------------------------------------- +; Check the Gitea releases API for a newer version than the one we are about +; to install. Best-effort only: any failure (offline, API down, no +; PowerShell) is swallowed and the installer proceeds silently. +; --------------------------------------------------------------------------- +Function .onInit + StrCpy $1 "$TEMP\pgtidy-latest-version.txt" + Delete "$1" + + DetailPrint "Checking ${PRODUCT_HOMEPAGE} for a newer release..." + nsExec::ExecToLog 'powershell -NoProfile -NonInteractive -Command "try { $$r = Invoke-RestMethod -Uri ''${RELEASES_API_URL}'' -UseBasicParsing -TimeoutSec 5; $$r.tag_name | Out-File -Encoding ascii -NoNewline ''$1'' } catch { exit 0 }"' + + ${IfNot} ${FileExists} "$1" + Return + ${EndIf} + + FileOpen $2 "$1" r + FileRead $2 $3 + FileClose $2 + Delete "$1" + + StrCpy $4 $3 + ; Trim a leading "v" if the tag is e.g. "v1.2.3" + StrCpy $5 $4 1 + ${If} $5 == "v" + StrCpy $4 $4 "" 1 + ${EndIf} + + ${If} $4 != "" + ${AndIf} $4 != "${VERSION}" + MessageBox MB_YESNO|MB_ICONINFORMATION \ + "A newer version of PgTidy is available: $4 (this installer is ${VERSION}).$\n$\nOpen the releases page to download it now?$\n$\nChoosing No continues installing ${VERSION}." \ + IDNO +2 + ExecShell "open" "${PRODUCT_HOMEPAGE}/releases/latest" + ${EndIf} +FunctionEnd + +; --------------------------------------------------------------------------- +; Adds $INSTDIR to the machine PATH if it isn't already present. +; --------------------------------------------------------------------------- +Function AddToPath + ReadRegStr $0 ${ENV_KEY} "Path" + Push "$0" + Push "$INSTDIR" + Call StrContains + Pop $1 + ${If} $1 == "" + ${If} $0 == "" + StrCpy $0 "$INSTDIR" + ${Else} + StrCpy $0 "$0;$INSTDIR" + ${EndIf} + WriteRegExpandStr ${ENV_KEY} "Path" "$0" + SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 + ${EndIf} +FunctionEnd + +; --------------------------------------------------------------------------- +; Removes $INSTDIR from the machine PATH. +; --------------------------------------------------------------------------- +Function un.RemoveFromPath + ReadRegStr $0 ${ENV_KEY} "Path" + Push "$0;" + Push "$INSTDIR;" + Push "" + Call un.StrReplace + Pop $0 + Push "$0" + Push "$INSTDIR" + Push "" + Call un.StrReplace + Pop $0 + ; Drop a trailing separator left behind by the replacements above. + StrCpy $1 $0 1 -1 + ${If} $1 == ";" + StrCpy $0 $0 -1 + ${EndIf} + WriteRegExpandStr ${ENV_KEY} "Path" "$0" + SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 +FunctionEnd + +; Returns the index of needle in haystack via $R0, or "" if absent. +; Push haystack, Push needle -> Pop result +Function StrContains + Exch $R1 ; needle + Exch + Exch $R2 ; haystack + Push $R3 + Push $R4 + Push $R5 + StrLen $R3 $R1 + StrCpy $R4 0 + ${Do} + StrCpy $R5 $R2 $R3 $R4 + ${If} $R5 == $R1 + StrCpy $R0 $R4 + ${ExitDo} + ${EndIf} + ${If} $R5 == "" + StrCpy $R0 "" + ${ExitDo} + ${EndIf} + IntOp $R4 $R4 + 1 + ${Loop} + Pop $R5 + Pop $R4 + Pop $R3 + Pop $R2 + Pop $R1 + Push $R0 + Exch + Pop $R0 +FunctionEnd + +; Push string, Push search, Push replace -> Pop result +Function un.StrReplace + Exch $R0 ; replace + Exch + Exch $R1 ; search + Exch 2 + Exch $R2 ; string + Push $R3 + Push $R4 + Push $R5 + Push $R6 + StrLen $R3 $R1 + StrCpy $R4 "" + ${Do} + StrCpy $R5 $R2 $R3 + ${If} $R5 == $R1 + StrCpy $R4 "$R4$R0" + StrCpy $R2 $R2 "" $R3 + ${ElseIf} $R2 == "" + ${ExitDo} + ${Else} + StrCpy $R6 $R2 1 + StrCpy $R4 "$R4$R6" + StrCpy $R2 $R2 "" 1 + ${EndIf} + ${Loop} + Pop $R6 + Pop $R5 + Pop $R4 + Pop $R3 + Pop $R2 + Pop $R1 + Pop $R0 + Push $R4 +FunctionEnd + +Section "PgTidy" SEC_MAIN + SectionIn RO + SetOutPath "$INSTDIR" + File "${SRC_EXE}" + File "..\LICENSE" + + Call AddToPath + + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${PRODUCT_NAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${VERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}" + WriteRegStr HKLM "${UNINST_KEY}" "InstallLocation" "$INSTDIR" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$INSTDIR\uninstall.exe" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$INSTDIR\uninstall.exe /S" + WriteRegDWORD HKLM "${UNINST_KEY}" "NoModify" 1 + WriteRegDWORD HKLM "${UNINST_KEY}" "NoRepair" 1 + + WriteUninstaller "$INSTDIR\uninstall.exe" +SectionEnd + +Section "Uninstall" + Call un.RemoveFromPath + Delete "$INSTDIR\pgtidy.exe" + Delete "$INSTDIR\LICENSE" + Delete "$INSTDIR\uninstall.exe" + RMDir "$INSTDIR" + DeleteRegKey HKLM "${UNINST_KEY}" +SectionEnd