Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions .github/workflows/build-installer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ jobs:
env:
BUILD_CONFIGURATION: Release
SOLUTION_PATH: 'src\vcxproj\salamand.sln'
REMOVE_PROJ_PATH: 'src\vcxproj\setup\remove.vcxproj'
# OPENSAL_BUILD_DIR MUST end with a backslash as expected by props files
OPENSAL_BUILD_DIR: '${{ github.workspace }}\build_stage\'

Expand All @@ -33,29 +32,36 @@ jobs:
# We do NOT pass /p:OutDir here, we let the project props use OPENSAL_BUILD_DIR
msbuild $env:SOLUTION_PATH /m /t:Build /p:Configuration=$env:BUILD_CONFIGURATION /p:Platform=x64 /p:PreferredToolArchitecture=x64

- name: Build Uninstaller (Release | x64)
run: |
# remove.vcxproj also respects OPENSAL_BUILD_DIR
msbuild $env:REMOVE_PROJ_PATH /m /t:Build /p:Configuration=$env:BUILD_CONFIGURATION /p:Platform=x64 /p:PreferredToolArchitecture=x64

- name: Download OpenSSL Libraries
run: |
$opensslZip = "openssl.zip"
Invoke-WebRequest -Uri "https://github.com/IndySockets/OpenSSL-Binaries/raw/master/Archive/openssl-1.0.2u-x64_86-win64.zip" -OutFile $opensslZip
Expand-Archive -Path $opensslZip -DestinationPath "utils" -Force
Remove-Item $opensslZip

- name: Prepare and Create Installer
- name: Install Inno Setup
run: |
choco install innosetup -y --no-progress
# Add Inno Setup to PATH
$innoPath = "C:\Program Files (x86)\Inno Setup 6"
echo "$innoPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

- name: Prepare Installer Files
run: |
# Stage files for Inno Setup
powershell.exe -File tools\prepare_installer.ps1 -BuildDir $env:OPENSAL_BUILD_DIR -StagingDir "Installer_Staging" -BuildNumber ${{ github.run_number }}

- name: Build Installer with Inno Setup
run: |
# Our script is already robust and searches for files within OPENSAL_BUILD_DIR
powershell.exe -File tools\prepare_installer.ps1 -BuildDir $env:OPENSAL_BUILD_DIR -OutputPath "OpenSalamander_5.0.${{ github.run_number }}.exe"
# Compile the Inno Setup script
iscc.exe /DSourcePath="Installer_Staging" /DBuildNumber=${{ github.run_number }} "Installer\setup.iss"

- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: 5.0.${{ github.run_number }}
name: Open Salamander 5.0.${{ github.run_number }}
files: OpenSalamander_5.0.${{ github.run_number }}.exe
files: Installer\Output\OpenSalamander_5.0.${{ github.run_number }}.exe
draft: false
prerelease: false
env:
Expand Down
Binary file removed Installer/setup.exe
Binary file not shown.
80 changes: 0 additions & 80 deletions Installer/setup.inf

This file was deleted.

170 changes: 170 additions & 0 deletions Installer/setup.iss
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
; Open Salamander Inno Setup Script
; This installer maintains all features from the previous custom installer

#define MyAppName "Open Salamander"
#define MyAppVersion "5.0"
#define MyAppPublisher "Taskscape Ltd"
#define MyAppURL "https://www.opensalamander.com/"
#define MyAppExeName "salamand.exe"

; Build number will be passed from command line during CI build
#ifndef BuildNumber
#define BuildNumber "0"
#endif

[Setup]
; NOTE: The value of AppId uniquely identifies this application.
AppId={{F4A1E7D3-8E5C-4B2A-9F6E-3D7C8A5B9E2F}
AppName={#MyAppName}
AppVersion={#MyAppVersion}.{#BuildNumber}
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
AllowNoIcons=yes
LicenseFile=LICENSE
; SetupIconFile=..\src\setup\res\setup.ico
OutputBaseFilename=OpenSalamander_{#MyAppVersion}.{#BuildNumber}
Compression=lzma2/ultra64
SolidCompression=yes
WizardStyle=modern
; 64-bit only installer
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
; Minimum Windows version
MinVersion=10.0.17763
; Uninstaller settings
UninstallDisplayIcon={app}\{#MyAppExeName}
UninstallDisplayName={#MyAppName} {#MyAppVersion}

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"

[CustomMessages]
english.LaunchAfterInstall=Launch {#MyAppName} after installation

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode

[Files]
; Main executables
Source: "{#SourcePath}\salamand.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}\salmon.exe"; DestDir: "{app}"; Flags: ignoreversion

; Shell extensions
Source: "{#SourcePath}\salextx64.dll"; DestDir: "{app}"; Flags: ignoreversion regserver 64bit
Source: "{#SourcePath}\salextx86.dll"; DestDir: "{app}"; Flags: ignoreversion regserver 32bit

; Utility executables (optional - only if present)
Source: "{#SourcePath}\salopen.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist
Source: "{#SourcePath}\salspawn.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist
Source: "{#SourcePath}\tserver.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist
Source: "{#SourcePath}\sfx7zip.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist
Source: "{#SourcePath}\zip2sfx.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist
Source: "{#SourcePath}\translator.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist
Source: "{#SourcePath}\salpvenv.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist
Source: "{#SourcePath}\fcremote.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist
Source: "{#SourcePath}\7zwrapper.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist

; OpenSSL libraries for FTP encryption support
Source: "{#SourcePath}\utils\libeay32.dll"; DestDir: "{app}\utils"; Flags: ignoreversion skipifsourcedoesntexist
Source: "{#SourcePath}\utils\ssleay32.dll"; DestDir: "{app}\utils"; Flags: ignoreversion skipifsourcedoesntexist

; License file
Source: "{#SourcePath}\LICENSE"; DestDir: "{app}"; Flags: ignoreversion

; Build info for traceability
Source: "{#SourcePath}\build_info.txt"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist

; Language files for main application
Source: "{#SourcePath}\lang\*.slg"; DestDir: "{app}\lang"; Flags: ignoreversion

; Toolbar icons
Source: "{#SourcePath}\toolbars\*.svg"; DestDir: "{app}\toolbars"; Flags: ignoreversion

; Convert tables (character encoding tables)
Source: "{#SourcePath}\convert\centeuro\*"; DestDir: "{app}\convert\centeuro"; Flags: ignoreversion recursesubdirs createallsubdirs skipifsourcedoesntexist
Source: "{#SourcePath}\convert\cyrillic\*"; DestDir: "{app}\convert\cyrillic"; Flags: ignoreversion recursesubdirs createallsubdirs skipifsourcedoesntexist
Source: "{#SourcePath}\convert\westeuro\*"; DestDir: "{app}\convert\westeuro"; Flags: ignoreversion recursesubdirs createallsubdirs skipifsourcedoesntexist

; Plugins - each plugin in its own directory with optional language files
Source: "{#SourcePath}\plugins\*"; DestDir: "{app}\plugins"; Flags: ignoreversion recursesubdirs createallsubdirs skipifsourcedoesntexist

[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchAfterInstall}"; Flags: nowait postinstall skipifsilent

[Registry]
; Add application to App Paths for easier command-line launching
Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\salamand.exe"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName}"; Flags: uninsdeletekey
Root: HKLM; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\salamand.exe"; ValueType: string; ValueName: "Path"; ValueData: "{app}"

[UninstallDelete]
; Clean up any files created during runtime
Type: filesandordirs; Name: "{app}\Temporary"
Type: dirifempty; Name: "{app}\plugins"
Type: dirifempty; Name: "{app}\lang"
Type: dirifempty; Name: "{app}\toolbars"
Type: dirifempty; Name: "{app}\convert"
Type: dirifempty; Name: "{app}\utils"
Type: dirifempty; Name: "{app}"

[Code]
// Check if Open Salamander is currently running and offer to close it
function InitializeSetup(): Boolean;
begin
Result := True;

// Check if salamand.exe is running (uses process list mutex from src/tasklist.cpp)
while CheckForMutexes('TaskscapeLtdSalamander3bProcessListMutex') do
begin
if MsgBox('Open Salamander is currently running. Please close it before continuing installation.' + #13#10 + #13#10 +
'Click OK to retry or Cancel to exit setup.', mbError, MB_OKCANCEL) = IDCANCEL then
begin
Result := False;
Exit;
end;
end;
end;

// Check if uninstalling while application is running
function InitializeUninstall(): Boolean;
begin
Result := True;

if CheckForMutexes('TaskscapeLtdSalamander3bProcessListMutex') then
begin
MsgBox('Open Salamander is currently running. Please close it before uninstalling.', mbError, MB_OK);
Result := False;
end;
end;

// Cleanup old installation if upgrading from custom installer
procedure CurStepChanged(CurStep: TSetupStep);
var
OldUninstallString: String;
ResultCode: Integer;
begin
if CurStep = ssInstall then
begin
// Check for old custom uninstaller (remove.exe)
if FileExists(ExpandConstant('{app}\remove.exe')) then
begin
// Run the old uninstaller silently
if MsgBox('A previous version of Open Salamander was detected. Would you like to uninstall it first?',
mbConfirmation, MB_YESNO) = IDYES then
begin
Exec(ExpandConstant('{app}\remove.exe'), '/S', '', SW_SHOW, ewWaitUntilTerminated, ResultCode);
end;
Comment on lines +159 to +167
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

CurStepChanged checks for a legacy uninstaller at '{app}\remove.exe', but DefaultDirName is now '{autopf}\Open Salamander'. If the legacy installer used a different default directory, this detection will miss most upgrades unless the user manually selects the old install folder. Consider detecting/removing the legacy install via its uninstall registry key or by probing the old default path before proceeding.

Suggested change
// Check for old custom uninstaller (remove.exe)
if FileExists(ExpandConstant('{app}\remove.exe')) then
begin
// Run the old uninstaller silently
if MsgBox('A previous version of Open Salamander was detected. Would you like to uninstall it first?',
mbConfirmation, MB_YESNO) = IDYES then
begin
Exec(ExpandConstant('{app}\remove.exe'), '/S', '', SW_SHOW, ewWaitUntilTerminated, ResultCode);
end;
// First try to locate old installation via its uninstall registry key
if RegQueryStringValue(HKLM,
'Software\Microsoft\Windows\CurrentVersion\Uninstall\Open Salamander',
'UninstallString', OldUninstallString) then
begin
// Run the old uninstaller using the registered uninstall command
if MsgBox('A previous version of Open Salamander was detected. Would you like to uninstall it first?',
mbConfirmation, MB_YESNO) = IDYES then
begin
Exec(OldUninstallString, '', '', SW_SHOW, ewWaitUntilTerminated, ResultCode);
end;
end
else
begin
// Fall back to checking for old custom uninstaller executable on disk
if FileExists(ExpandConstant('{pf}\Open Salamander\remove.exe')) then
begin
// Run the old uninstaller from the legacy default path
if MsgBox('A previous version of Open Salamander was detected. Would you like to uninstall it first?',
mbConfirmation, MB_YESNO) = IDYES then
begin
Exec(ExpandConstant('{pf}\Open Salamander\remove.exe'), '/S', '', SW_SHOW, ewWaitUntilTerminated, ResultCode);
end;
end
else if FileExists(ExpandConstant('{app}\remove.exe')) then
begin
// Run the old uninstaller from the current application directory
if MsgBox('A previous version of Open Salamander was detected. Would you like to uninstall it first?',
mbConfirmation, MB_YESNO) = IDYES then
begin
Exec(ExpandConstant('{app}\remove.exe'), '/S', '', SW_SHOW, ewWaitUntilTerminated, ResultCode);
end;
end;

Copilot uses AI. Check for mistakes.
end;
end;
end;
1 change: 0 additions & 1 deletion Installer/x64

This file was deleted.

35 changes: 24 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,37 @@ Solution ```\src\vcxproj\salamand.sln``` may be built from within Visual Studio

Use ```\src\vcxproj\!populate_build_dir.cmd``` to populate build directory with files required to run Open Salamander.

### Creating SFX Installer
### Creating Installer

To create a standalone self-extracting installer (EXE) for distribution:
Open Salamander uses [Inno Setup](https://jrsoftware.org/isinfo.php) to create the installer. The installer script is located at `Installer\setup.iss`.

1. **Prepare files:** Ensure the `Installer` directory contains the latest build of `salamand.exe`, `salmon.exe`, and other required files.
2. **Run the script:** Use the provided PowerShell script in the `tools` directory.
#### Building Locally

1. Install [Inno Setup 6](https://jrsoftware.org/isdl.php) or later
2. Build the solution in Release|x64 configuration
3. Stage the files and compile the installer:

```powershell
# Run from the project root
.\tools\Create-Sfx.ps1 -SourceDir "Installer" -OutputPath "OpenSalamander_Setup.exe"
# Stage files for the installer
.\tools\prepare_installer.ps1 -BuildDir "build_stage" -StagingDir "Installer_Staging"

# Compile the installer
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" "Installer\setup.iss"
```

The script automatically:
The installer will be created in `Installer\Output\`.

#### GitHub Actions CI/CD

The repository includes a GitHub Actions workflow (`.github\workflows\build-installer.yml`) that automatically:

1. Builds the solution using MSBuild
2. Stages all required files (executables, plugins, language files, toolbars)
3. Installs Inno Setup via Chocolatey
4. Compiles the installer with build number versioning
5. Creates a GitHub release with the installer attached

- Compiles a C# bootstrap (stub) for extraction.
- Includes the latest SVG icons from `src\res\toolbars`.
- Modifies `setup.inf` (internally in the package) if necessary to ensure icons are installed.
- Produces a single `OpenSalamander_Setup.exe`.
The workflow is triggered on pushes to `main` and produces versioned installers named `OpenSalamander_5.0.{build_number}.exe`.

## Customization

Expand Down
7 changes: 5 additions & 2 deletions src/common/handles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2219,9 +2219,12 @@ C__Handles::LoadLibraryUtf8(LPCSTR lpLibFileName)
else
{
ret = ::LoadLibraryW(fileNameW);
free(fileNameW);
}
CheckCreate(ret != NULL, __htLibrary, __hoLoadLibrary, ret, GetLastError(), TRUE, lpLibFileName);
DWORD err = GetLastError();
CheckCreate(ret != NULL, __htLibrary, __hoLoadLibrary, ret, err, TRUE, NULL, lpLibFileName, fileNameW);
SetLastError(err);
if (fileNameW != NULL)
free(fileNameW);
return ret;
}

Expand Down
4 changes: 0 additions & 4 deletions src/plugins/pictview/vcxproj/pictview.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,6 @@
<Project>{bc2e9ea5-ebc4-4fdf-b064-e2bbbbcba1d3}</Project>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
<ProjectReference Include="salpvenv.vcxproj">
<Project>{17cf5e05-f29c-4e5d-ba41-83254e342e0b}</Project>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets">
</Import>
Expand Down
Loading
Loading