Component: NSIS Windows Installer
File: installer/Installer.nsi
Platform: Windows 10+
Build Tool: NSIS 3.08+
Overview
The NSIS (Nullsoft Scriptable Install System) installer is GAIA’s primary Windows distribution mechanism. It creates a single-file executable that packages Python, GAIA source, dependencies, and configuration into a seamless installation experience.
Key Features:
- Single-exe distribution
- No admin rights required (user-level install)
- Embedded Python 3.10.9
- Silent install support
- Custom install paths
- Progress reporting with DetailPrint
- Automatic dependency checking (Lemonade, FFmpeg)
Requirements
Functional Requirements
-
Installation Flow
- Welcome page with version display
- License agreement (MIT)
- Directory selection (default: $LOCALAPPDATA\GAIA)
- Optional components selection (RAUX UI)
- Progress indication during install
- Finish page with launch options
-
Prerequisite Handling
- Check for existing installation (prompt to remove)
- Download/install Lemonade if missing
- Install FFmpeg via winget if missing
- Verify versions with Python script
-
File Operations
- Extract GAIA source to install directory
- Download embedded Python
- Configure Python environment
- Create bin directory structure
- Install launch scripts
-
Post-Install Configuration
- Update user PATH environment variable
- Create desktop shortcuts (non-silent only)
- Launch RAUX installer if selected
- Offer to launch GAIA CLI
Non-Functional Requirements
-
User Experience
- Modern UI (MUI2)
- Clear progress messages
- Informative error messages
- Silent mode for automation
-
Reliability
- Comprehensive error checking
- Atomic operations (all-or-nothing)
- Cleanup on failure
- Logging to DetailPrint
-
Compatibility
- Windows 10+
- 64-bit only
- No admin rights required
- Respects existing installations
System Architecture
Installation Flow Diagram
┌─────────────────┐
│ User Launches │
│ setup.exe │
└────────┬────────┘
│
┌────▼─────────────────────┐
│ Check Existing Install │
│ (Prompt to remove) │
└────┬─────────────────────┘
│
┌────▼──────────────────┐
│ Select Components │
│ ☑ GAIA (required) │
│ ☐ RAUX UI (optional)│
└────┬──────────────────┘
│
┌────▼─────────────────────┐
│ Download/Extract │
│ - Embedded Python │
│ - docopt.py (workaround)│
└────┬─────────────────────┘
│
┌────▼──────────────────┐
│ Check Prerequisites │
│ - Lemonade (prompt) │
│ - FFmpeg (winget) │
└────┬──────────────────┘
│
┌────▼───────────────────┐
│ Install GAIA Package │
│ (via install.bat) │
└────┬───────────────────┘
│
┌────▼──────────────────┐
│ Configure System │
│ - Update PATH │
│ - Create shortcuts │
│ - Launch RAUX setup │
└────┬──────────────────┘
│
┌────▼───────────┐
│ Finish Page │
│ - Launch CLI? │
│ - Launch RAUX?│
└────────────────┘
NSIS Script Structure
Installer.nsi
├── Header Section
│ ├── Command-line parameters (!define)
│ ├── Variables (Var)
│ └── MUI2 settings
│
├── Pages
│ ├── Welcome (MUI_PAGE_WELCOME)
│ ├── License (MUI_PAGE_LICENSE)
│ ├── Directory (MUI_PAGE_DIRECTORY)
│ ├── RAUX Options (custom page)
│ ├── Install (MUI_PAGE_INSTFILES)
│ └── Finish (custom page)
│
├── Main Install Section
│ ├── Cleanup existing install
│ ├── Create directory structure
│ ├── Download Python
│ ├── Configure Python
│ ├── Install dependencies
│ └── Install GAIA
│
└── Functions
├── .onInit
├── RAUXOptionsPage
├── CustomFinishPage
├── run_raux_installer
├── check_raux_install
├── uninstall_raux
├── RunGAIACLI
└── RunRAUX
API Specification
Command-Line Interface
Syntax:
gaia-windows-setup.exe [/S] [/D=<path>]
Parameters:
/S - Silent install (no UI, automatic defaults)
/D=<path> - Custom installation directory (must be last parameter)
Examples:
# Interactive install
.\gaia-windows-setup.exe
# Silent install to default location
.\gaia-windows-setup.exe /S
# Custom install path
.\gaia-windows-setup.exe /D=C:\MyApps\GAIA
# Silent install to custom path
.\gaia-windows-setup.exe /S /D=D:\Tools\GAIA
Build-Time Variables
Defined in version.nsh:
!define GAIA_VERSION "1.0.0"
!define LEMONADE_VERSION "8.0.4"
Hardcoded in Installer.nsi:
!define RAUX_VERSION "v0.6.5+raux.0.2.4"
!define RAUX_PRODUCT_NAME "GAIA UI"
!define RAUX_PRODUCT_SQUIRREL_NAME "GaiaUi"
!define PYTHON_VERSION "3.10.9"
!define PYTHON_EMBED_URL "https://www.python.org/ftp/python/3.10.9/python-3.10.9-embed-amd64.zip"
!define GET_PIP_URL "https://bootstrap.pypa.io/get-pip.py"
Key Functions
Function: .onInit
Function .onInit
; Initialize GAIA version string
StrCpy $GAIA_STRING "GAIA ${GAIA_VERSION}"
; Set RAUX install to checked by default
StrCpy $InstallRAUX "1"
FunctionEnd
Function: RAUXOptionsPage
Function RAUXOptionsPage
; Create custom page for RAUX selection
!insertmacro MUI_HEADER_TEXT "Additional Components" "Choose additional components to install"
nsDialogs::Create 1018
Pop $0
; Create checkbox for RAUX
${NSD_CreateCheckbox} 10 10 100% 12u "Install ${RAUX_PRODUCT_NAME}"
Pop $1
${NSD_SetState} $1 $InstallRAUX
; Show description
${NSD_CreateLabel} 25 30 100% 40u "RAUX UI provides a chat interface..."
Pop $2
nsDialogs::Show
FunctionEnd
Function: check_version_compatibility
; Check Lemonade version using Python script
nsExec::ExecToStack 'cmd /c ""$INSTDIR\python\python.exe" "$INSTDIR\installer_utils.py" "${LEMONADE_VERSION}""'
Pop $4 ; Return code (0=compatible, 1=not compatible)
Pop $5 ; Output with version info
; Extract version from output
${StrLoc} $6 "$5" "VERSION:" ">"
${If} $6 != ""
IntOp $6 $6 + 8
StrCpy $7 "$5" "" $6 ; Version string
${EndIf}
Implementation Details
Directory Cleanup Logic
Problem: Existing installation must be removed before new install.
Solution:
; Check if directory exists
IfFileExists "$INSTDIR\*.*" 0 continue_install
; Interactive mode: Ask user
${IfNot} ${Silent}
MessageBox MB_YESNO "An existing GAIA installation was found. Remove it?" IDYES remove_dir
MessageBox MB_OK "Installation cancelled."
Quit
${EndIf}
remove_dir:
; Remove all files
RMDir /r "$INSTDIR"
; Verify removal
IfFileExists "$INSTDIR\*.*" 0 continue_install
MessageBox MB_OK "Unable to remove existing installation."
Quit
continue_install:
; Proceed with fresh install
Python Configuration
Embedded Python Limitation: Does not include site-packages by default.
Fix: Modify python310._pth file:
; Add site-packages to path
FileOpen $2 "$INSTDIR\python\python310._pth" a
FileSeek $2 0 END
FileWrite $2 "$\r$\nLib$\r$\n"
FileWrite $2 "$\r$\nLib\site-packages$\r$\n"
FileClose $2
Docopt Circular Dependency Workaround
Problem: Docopt has circular import that breaks pip install.
Solution: Pre-download docopt.py:
DetailPrint "- Downloading docopt.py to fix circular dependency..."
nsExec::ExecToStack 'curl -L -o "$INSTDIR\python\docopt.py" "https://raw.githubusercontent.com/docopt/docopt/master/docopt.py"'
Pop $0 ; Return value
Pop $1 ; Output
${If} $0 != 0
DetailPrint "- ERROR: Failed to download docopt.py"
Quit
${EndIf}
PATH Environment Update
Requirements:
- Update user PATH (not system)
- Append, don’t replace
- Avoid duplicates
- Notify shell of change
Implementation:
; Read current PATH
ReadRegStr $0 HKCU "Environment" "PATH"
; Directories to add
StrCpy $1 "$INSTDIR\bin"
StrCpy $2 "$INSTDIR\python\Scripts"
; Check if already in PATH
${StrLoc} $3 "$0" "$1" ">"
${StrLoc} $4 "$0" "$2" ">"
; Only update if not present
${If} $3 == ""
${OrIf} $4 == ""
; Append to PATH
WriteRegExpandStr HKCU "Environment" "PATH" "$0;$1;$2"
; Notify system of change
SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
${EndIf}
Lemonade Version Checking
Flow:
- Check if lemonade-server is in PATH
- If found, check version compatibility
- If incompatible or missing, prompt to install
- Download from GitHub releases
- Run MSI with silent flag
Implementation:
check_lemonade:
; Try to run lemonade-server --version
nsExec::ExecToStack 'cmd /c "lemonade-server --version 2>&1"'
Pop $2 ; Return code
Pop $3 ; Output
; If not found (code != 0)
${If} $2 != "0"
MessageBox MB_YESNO "Lemonade is required. Install now?" IDYES install_lemonade
GoTo skip_lemonade
${EndIf}
; Check version with Python script
nsExec::ExecToStack '... installer_utils.py ...'
Pop $4 ; 0=compatible, 1=incompatible
${If} $4 == "0"
; Version OK
GoTo create_env
${Else}
; Version incompatible
MessageBox MB_YESNO "Lemonade version mismatch. Update?" IDYES install_lemonade
${EndIf}
install_lemonade:
; Download MSI
nsExec::ExecToStack 'curl -L -o "$TEMP\lemonade.msi" "$LEMONADE_URL"'
; Install silently
ExecWait 'msiexec /i "$TEMP\lemonade.msi" /qn' $2
${If} $2 == "0"
MessageBox MB_OK "Lemonade installed successfully"
${Else}
MessageBox MB_OK "Lemonade install failed (exit code $2)"
${EndIf}
Testing Requirements
Build Tests
# Test NSIS compilation
makensis /V4 installer\Installer.nsi
# Check output exists
if (!(Test-Path "gaia-windows-setup.exe")) {
throw "Build failed: installer not created"
}
# Verify file size (should be reasonable)
$size = (Get-Item "gaia-windows-setup.exe").Length / 1MB
if ($size -lt 5 -or $size -gt 100) {
throw "Build warning: Unexpected installer size ${size}MB"
}
Installation Tests
# Test 1: Default install
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\GAIA" -ErrorAction SilentlyContinue
.\gaia-windows-setup.exe /S
Start-Sleep -Seconds 60 # Wait for install
# Verify
if (!(Test-Path "$env:LOCALAPPDATA\GAIA\python\python.exe")) {
throw "Install failed: Python not found"
}
# Test 2: Custom path
.\gaia-windows-setup.exe /S /D=C:\Test\GAIA
Start-Sleep -Seconds 60
if (!(Test-Path "C:\Test\GAIA\python\python.exe")) {
throw "Custom path install failed"
}
# Test 3: Upgrade
.\gaia-windows-setup-v1.0.0.exe /S
.\gaia-windows-setup-v1.1.0.exe /S
# Verify version
$version = & "$env:LOCALAPPDATA\GAIA\python\Scripts\gaia.exe" --version
if ($version -notmatch "1.1.0") {
throw "Upgrade failed: version mismatch"
}
Regression Tests
# Test PATH update
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
if ($userPath -notlike "*GAIA\bin*") {
throw "PATH not updated correctly"
}
# Test shortcuts
if (!(Test-Path "$env:USERPROFILE\Desktop\GAIA CLI.lnk")) {
throw "Desktop shortcut not created"
}
# Test GAIA command
try {
gaia --help | Out-Null
} catch {
throw "GAIA command not in PATH"
}
# Test Python environment
$output = & "$env:LOCALAPPDATA\GAIA\python\python.exe" -c "import gaia; print(gaia.__version__)"
if ([string]::IsNullOrEmpty($output)) {
throw "GAIA package not properly installed"
}
Dependencies
Build Dependencies
[build-system]
requires = [
"nsis>=3.08", # NSIS compiler
"git", # Version control
]
External Resources
runtime_downloads:
- name: Python Embedded
url: https://www.python.org/ftp/python/3.10.9/python-3.10.9-embed-amd64.zip
size: ~9MB
required: true
- name: get-pip.py
url: https://bootstrap.pypa.io/get-pip.py
size: ~2MB
required: true
- name: docopt.py
url: https://raw.githubusercontent.com/docopt/docopt/master/docopt.py
size: ~20KB
required: true
- name: Lemonade Server
url: https://github.com/lemonade-sdk/lemonade/releases/download/v8.0.4/lemonade-server-minimal.msi
size: ~500MB
required: false
prompt: true
- name: RAUX Setup
url: https://github.com/aigdat/raux/releases/download/v0.6.5+raux.0.2.4/raux-setup.exe
size: ~100MB
required: false
optional: true
Error Handling
Common Errors and Solutions
Error: “Failed to download Python”
${If} $0 != 0
DetailPrint "- ERROR: Failed to download Python"
${IfNot} ${Silent}
MessageBox MB_OK "Failed to download Python. Check internet connection and try again."
${EndIf}
Quit
${EndIf}
User Action: Check network, retry install, or download Python manually.
Error: “GAIA installation failed”
REM install.bat
if %ERRORLEVEL% NEQ 0 (
echo *** INSTALLATION FAILED ***
echo Please check gaia_install.log for details
exit /b 3
)
User Action: Check gaia_install.log, verify disk space, check antivirus.
Error: “PATH update failed”
${If} ${Errors}
DetailPrint "- ERROR: Failed to write PATH to registry"
MessageBox MB_OKCANCEL "Failed to update PATH. Continue anyway?" IDOK continue IDCANCEL abort
abort:
Abort "Installation aborted"
continue:
DetailPrint "- Continuing without PATH update"
${EndIf}
User Action: Manually add directories to PATH, or run installer as admin.
Configuration
Build Configuration
File: installer/version.nsh
!define GAIA_VERSION "1.0.0"
!define LEMONADE_VERSION "8.0.4"
Build Command:
# Set version
$version = "1.2.3"
"!define GAIA_VERSION `"$version`"" | Out-File -Encoding ASCII installer\version.nsh
# Compile
makensis /V4 installer\Installer.nsi
Runtime Configuration
GAIA uses:
- Installation directory:
$INSTDIR (default: $LOCALAPPDATA\GAIA)
- Python exe:
$INSTDIR\python\python.exe
- Scripts dir:
$INSTDIR\python\Scripts
- Bin dir:
$INSTDIR\bin
User can modify:
- Install location via
/D= flag
- RAUX installation via checkbox
- Lemonade installation via prompt
Usage Examples
Example 1: Automated Build Pipeline
# CI/CD build script
param(
[Parameter(Mandatory=$true)]
[string]$Version
)
# Create version file
@"
!define GAIA_VERSION "$Version"
!define LEMONADE_VERSION "8.0.4"
"@ | Out-File -Encoding ASCII installer\version.nsh
# Build installer
& "C:\Program Files (x86)\NSIS\makensis.exe" /V4 installer\Installer.nsi
# Check result
if ($LASTEXITCODE -ne 0) {
throw "NSIS build failed"
}
# Rename with version
Move-Item gaia-windows-setup.exe "gaia-windows-setup-v$Version.exe" -Force
Write-Host "✓ Built installer: gaia-windows-setup-v$Version.exe"
Example 2: Enterprise Deployment
# Deploy to 100 machines
$computers = Get-ADComputer -Filter * -SearchBase "OU=Engineering,DC=company,DC=com"
foreach ($computer in $computers) {
Invoke-Command -ComputerName $computer.Name -ScriptBlock {
# Download installer
$url = "https://releases.company.com/gaia-setup.exe"
$setup = "$env:TEMP\gaia-setup.exe"
Invoke-WebRequest -Uri $url -OutFile $setup
# Install silently
Start-Process -Wait -FilePath $setup -ArgumentList "/S"
# Verify
if (Test-Path "$env:LOCALAPPDATA\GAIA\python\Scripts\gaia.exe") {
Write-Host "✓ Installed on $env:COMPUTERNAME"
} else {
Write-Warning "✗ Installation failed on $env:COMPUTERNAME"
}
# Cleanup
Remove-Item $setup
}
}
Example 3: Unattended Install with Logging
# Install with detailed logging
$logFile = "C:\Logs\gaia-install.log"
# Run installer
Start-Process -Wait -FilePath "gaia-windows-setup.exe" `
-ArgumentList "/S /D=$env:ProgramData\GAIA" `
-RedirectStandardOutput $logFile `
-RedirectStandardError "${logFile}.err"
# Check log for errors
$errors = Select-String -Path "${logFile}.err" -Pattern "ERROR"
if ($errors) {
Write-Error "Installation errors detected:"
$errors | ForEach-Object { Write-Error $_.Line }
exit 1
}
Write-Host "✓ Installation successful"
Acceptance Criteria
NSIS Windows Installer Technical Specification