diff --git a/dist/livetrace-3.3.2-setup.exe b/dist/livetrace-3.3.2-setup.exe new file mode 100644 index 0000000..39cf8ee Binary files /dev/null and b/dist/livetrace-3.3.2-setup.exe differ diff --git a/installer/Dockerfile.installer b/installer/Dockerfile.installer new file mode 100644 index 0000000..ae91af1 --- /dev/null +++ b/installer/Dockerfile.installer @@ -0,0 +1,30 @@ +# Dockerfile.installer +# Builds the livetrace Windows installer (.exe) using NSIS on Linux. +# +# Usage (from your project root): +# docker build -f Dockerfile.installer -t livetrace-installer . +# docker run --rm -v "$PWD/dist":/out livetrace-installer +# +# The finished installer lands at ./dist/livetrace-3.3.2-setup.exe + +FROM debian:bookworm-slim + +# Install NSIS (makensis) — no Go needed, binary is pre-built +RUN apt-get update && \ + apt-get install -y --no-install-recommends nsis && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# Copy the NSIS script and helper +COPY installer/livetrace-installer.nsi . +COPY installer/EnvVarUpdate.nsh . + +# Copy the pre-built Windows binary from dist/ +COPY dist/livetrace-windows-amd64.exe . + +# Build the installer +RUN makensis livetrace-installer.nsi + +# Default: copy the output to /out (bind-mount your dist/ folder there) +CMD ["sh", "-c", "cp livetrace-*.exe /out/ && echo 'Installer written to /out/'"] diff --git a/installer/EnvVarUpdate.nsh b/installer/EnvVarUpdate.nsh new file mode 100644 index 0000000..477cf35 --- /dev/null +++ b/installer/EnvVarUpdate.nsh @@ -0,0 +1,142 @@ +; EnvVarUpdate.nsh +; Adds/removes a value from a Windows environment variable (PATH-style, semicolon-delimited) +; Usage: +; ${EnvVarUpdate} $0 "PATH" "A" "HKLM" "$INSTDIR" ; append +; ${UnEnvVarUpdate} $0 "PATH" "HKLM" "$INSTDIR" ; remove + +!ifndef ENVVARUPDATE_NSH +!define ENVVARUPDATE_NSH + +!include "LogicLib.nsh" +!include "WinMessages.nsh" + +; -------------------------------------------------------------------------- +; Macro: _EnvVarUpdate +; Action: "A" = Append, "P" = Prepend, "R" = Remove +; -------------------------------------------------------------------------- +!macro _EnvVarUpdate OUTVAR EnvVarName Action RegLoc PathString + + Push $0 + Push $1 + Push $2 + Push $3 + Push $4 + Push $5 + + ; Read current value + ReadRegStr $1 ${RegLoc} \ + "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" \ + "${EnvVarName}" + + ; Strip trailing semicolon + StrCpy $5 $1 1 -1 + ${If} $5 == ";" + StrCpy $1 $1 -1 + ${EndIf} + + ; Check if PathString already exists + Push "${PathString}" + Push $1 + Call _EnvVarUpdateStrStr + Pop $2 ; $2 = position, or "" if not found + + ${If} "${Action}" == "A" + ${If} $2 == "" + ${If} $1 == "" + StrCpy $3 "${PathString}" + ${Else} + StrCpy $3 "$1;${PathString}" + ${EndIf} + WriteRegExpandStr ${RegLoc} \ + "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" \ + "${EnvVarName}" "$3" + ${EndIf} + ${ElseIf} "${Action}" == "P" + ${If} $2 == "" + ${If} $1 == "" + StrCpy $3 "${PathString}" + ${Else} + StrCpy $3 "${PathString};$1" + ${EndIf} + WriteRegExpandStr ${RegLoc} \ + "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" \ + "${EnvVarName}" "$3" + ${EndIf} + ${ElseIf} "${Action}" == "R" + ${If} $2 != "" + ; Remove entry + StrLen $4 "${PathString}" + StrCpy $0 $1 $2 ; everything before the match + IntOp $4 $2 + $4 ; position after the match + StrCpy $5 $1 "" $4 ; everything after the match + ; Strip leading/trailing semicolons from join point + StrCpy $4 $5 1 + ${If} $4 == ";" + StrCpy $5 $5 "" 1 + ${EndIf} + StrCpy $4 $0 1 -1 + ${If} $4 == ";" + StrCpy $0 $0 -1 + ${EndIf} + ${If} $0 == "" + StrCpy $3 $5 + ${ElseIf} $5 == "" + StrCpy $3 $0 + ${Else} + StrCpy $3 "$0;$5" + ${EndIf} + WriteRegExpandStr ${RegLoc} \ + "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" \ + "${EnvVarName}" "$3" + ${EndIf} + ${EndIf} + + ; Broadcast change to all windows + SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" /TIMEOUT=5000 + + Pop $5 + Pop $4 + Pop $3 + Pop $2 + Pop $1 + Pop $0 + +!macroend + +; Helper: find substring position (returns "" if not found) +Function _EnvVarUpdateStrStr + Exch $R0 ; haystack + Exch + Exch $R1 ; needle + Push $R2 + Push $R3 + Push $R4 + Push $R5 + StrLen $R3 $R1 + StrLen $R4 $R0 + IntOp $R4 $R4 - $R3 + StrCpy $R2 0 +loop: + IntCmp $R2 $R4 done done + StrCpy $R5 $R0 $R3 $R2 + StrCmp $R5 $R1 found + IntOp $R2 $R2 + 1 + Goto loop +found: + StrCpy $R0 $R2 + Goto end +done: + StrCpy $R0 "" +end: + Pop $R5 + Pop $R4 + Pop $R3 + Pop $R2 + Pop $R1 + Exch $R0 +FunctionEnd + +!define EnvVarUpdate `!insertmacro _EnvVarUpdate` +!define UnEnvVarUpdate `!insertmacro _EnvVarUpdate "" "${__LINE__}" "R"` + +!endif ; ENVVARUPDATE_NSH diff --git a/installer/build-installer.sh b/installer/build-installer.sh new file mode 100755 index 0000000..6ea667a --- /dev/null +++ b/installer/build-installer.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# build-installer.sh +# Builds livetrace-3.3.2-setup.exe on Linux — natively or via Docker. +# +# Usage: +# ./installer/build-installer.sh # requires: apt install nsis +# ./installer/build-installer.sh --docker # requires: docker + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +VERSION="3.3.2" +OUTPUT="livetrace-${VERSION}-setup.exe" + +# ── Docker build ─────────────────────────────────────────────────────────── +if [[ "${1:-}" == "--docker" ]]; then + echo ">>> Building installer via Docker …" + docker build \ + -f "${SCRIPT_DIR}/Dockerfile.installer" \ + -t livetrace-installer \ + "${PROJECT_ROOT}" + + docker run --rm \ + -v "${PROJECT_ROOT}/dist":/out \ + livetrace-installer + + echo "" + echo "✓ Done: ${PROJECT_ROOT}/dist/${OUTPUT}" + exit 0 +fi + +# ── Native build ─────────────────────────────────────────────────────────── +if ! command -v makensis &>/dev/null; then + echo "ERROR: makensis not found." + echo " Install: sudo apt install nsis" + echo " Or run: $0 --docker" + exit 1 +fi + +SRC_EXE="${PROJECT_ROOT}/dist/livetrace-windows-amd64.exe" +if [[ ! -f "${SRC_EXE}" ]]; then + echo "ERROR: ${SRC_EXE} not found." + echo " Run your Go build first, e.g.:" + echo " GOOS=windows GOARCH=amd64 go build -o dist/livetrace-windows-amd64.exe ./src" + exit 1 +fi + +echo ">>> Building installer with makensis …" +# Copy binary next to the .nsi script so NSIS can find it +cp "${SRC_EXE}" "${SCRIPT_DIR}/livetrace-windows-amd64.exe" + +cd "${SCRIPT_DIR}" +makensis livetrace-installer.nsi + +# Move the output to dist/ +mv "${SCRIPT_DIR}/${OUTPUT}" "${PROJECT_ROOT}/dist/${OUTPUT}" +rm -f "${SCRIPT_DIR}/livetrace-windows-amd64.exe" + +echo "" +echo "✓ Done: ${PROJECT_ROOT}/dist/${OUTPUT}" +echo " Size: $(du -sh "${PROJECT_ROOT}/dist/${OUTPUT}" | cut -f1)" diff --git a/installer/livetrace-installer.nsi b/installer/livetrace-installer.nsi new file mode 100644 index 0000000..ba2165c --- /dev/null +++ b/installer/livetrace-installer.nsi @@ -0,0 +1,138 @@ +; livetrace-installer.nsi +; NSIS installer for livetrace v3.3.2 +; Builds on Linux (or in Docker) with: makensis livetrace-installer.nsi +; +; Place livetrace-windows-amd64.exe in the same directory before building. + +Unicode True + +; ────────────────────────────────────────────── +; Metadata +; ────────────────────────────────────────────── +!define APPNAME "livetrace" +!define APPVERSION "3.3.2" +!define PUBLISHER "ngtech1ltd" + +; Source binary name (as built by Go) +!define SRC_EXE "livetrace-windows-amd64.exe" +; Name it will have after installation +!define INST_EXE "livetrace.exe" + +; Registry key for Add/Remove Programs +!define REG_UNINSTALL \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" + +; ────────────────────────────────────────────── +; Installer settings +; ────────────────────────────────────────────── +Name "${APPNAME} ${APPVERSION}" +OutFile "livetrace-${APPVERSION}-setup.exe" +InstallDir "$PROGRAMFILES64\${APPNAME}" +InstallDirRegKey HKLM "${REG_UNINSTALL}" "InstallLocation" + +; Administrator rights required (Program Files + system PATH) +RequestExecutionLevel admin + +SetCompressor /SOLID lzma +SetCompressorDictSize 32 + +; ────────────────────────────────────────────── +; UI +; ────────────────────────────────────────────── +!include "MUI2.nsh" + +!define MUI_ABORTWARNING +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "English" + +; ────────────────────────────────────────────── +; Version info (visible in Properties → Details) +; ────────────────────────────────────────────── +VIProductVersion "3.3.2.0" +VIAddVersionKey "ProductName" "${APPNAME}" +VIAddVersionKey "ProductVersion" "${APPVERSION}" +VIAddVersionKey "CompanyName" "${PUBLISHER}" +VIAddVersionKey "FileDescription" "${APPNAME} Installer" +VIAddVersionKey "FileVersion" "${APPVERSION}" +VIAddVersionKey "LegalCopyright" "(c) ${PUBLISHER}" + +; ────────────────────────────────────────────── +; PATH helpers (PowerShell – avoids NSIS macro +; namespace issues between install / uninstall) +; ────────────────────────────────────────────── + +; Append $INSTDIR to the machine-wide PATH +; Note: $$p / $$parts / $$_ are PowerShell variables — $$ escapes the $ for NSIS +!macro AddToPath + DetailPrint "Adding $INSTDIR to system PATH …" + nsExec::ExecToLog \ + 'powershell.exe -NonInteractive -NoProfile -Command \ + "$$p = [Environment]::GetEnvironmentVariable(\"Path\",\"Machine\"); \ + if ($$p -notlike \"*$INSTDIR*\") { \ + [Environment]::SetEnvironmentVariable(\"Path\", \"$$p;$INSTDIR\", \"Machine\") \ + }"' +!macroend + +; Remove $INSTDIR from the machine-wide PATH +!macro RemoveFromPath + DetailPrint "Removing $INSTDIR from system PATH …" + nsExec::ExecToLog \ + 'powershell.exe -NonInteractive -NoProfile -Command \ + "$$parts = [Environment]::GetEnvironmentVariable(\"Path\",\"Machine\") -split \";\"; \ + $$parts = $$parts | Where-Object { $$_ -ne \"$INSTDIR\" }; \ + [Environment]::SetEnvironmentVariable(\"Path\", ($$parts -join \";\"), \"Machine\")"' +!macroend + +; ────────────────────────────────────────────── +; Install section +; ────────────────────────────────────────────── +Section "livetrace (required)" SecMain + + SectionIn RO ; cannot be deselected + + SetOutPath "$INSTDIR" + + ; Copy binary — rename to plain "livetrace.exe" on the way in + File "/oname=${INST_EXE}" "${SRC_EXE}" + + ; Add install dir to system PATH + !insertmacro AddToPath + + ; Register with Add/Remove Programs + WriteRegStr HKLM "${REG_UNINSTALL}" "DisplayName" "${APPNAME} ${APPVERSION}" + WriteRegStr HKLM "${REG_UNINSTALL}" "DisplayVersion" "${APPVERSION}" + WriteRegStr HKLM "${REG_UNINSTALL}" "Publisher" "${PUBLISHER}" + WriteRegStr HKLM "${REG_UNINSTALL}" "InstallLocation" "$INSTDIR" + WriteRegStr HKLM "${REG_UNINSTALL}" "UninstallString" '"$INSTDIR\uninstall.exe"' + WriteRegStr HKLM "${REG_UNINSTALL}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' + WriteRegDWORD HKLM "${REG_UNINSTALL}" "NoModify" 1 + WriteRegDWORD HKLM "${REG_UNINSTALL}" "NoRepair" 1 + + ; Write the uninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + +SectionEnd + +; ────────────────────────────────────────────── +; Uninstall section +; ────────────────────────────────────────────── +Section "Uninstall" + + ; Remove from system PATH + !insertmacro RemoveFromPath + + ; Delete files + Delete "$INSTDIR\${INST_EXE}" + Delete "$INSTDIR\uninstall.exe" + RMDir "$INSTDIR" + + ; Remove Add/Remove Programs entry + DeleteRegKey HKLM "${REG_UNINSTALL}" + +SectionEnd