Files
lightcraft/packaging/windows/lightcraft.wxs
T
Chad ElliottandClaude Opus 5.5 a82ecb3753 fix(windows): #399 ignore stale App Paths values in the install-folder fallback
The App Paths directory search doesn't check that lightcraft.exe exists, and
when the old installers' unquoted path is gone, Windows Installer falls back
to the text before the first space: a deleted
C:\Program Files\LightCraft\lightcraft.exe resolved to C:\, so an upgrade
would install into the drive root. Gate the fallback on a second search of
the same value with a FileSearch for lightcraft.exe.

The install-folder test gains legacy-folder-deleted (failed before this fix)
and legacy-exe-deleted cases, and removes its empty parent registry key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:18:22 -05:00

205 lines
12 KiB
XML

<?xml version="1.0" encoding="utf-8"?>
<!--
LightCraft MSI (WiX Toolset v5: `dotnet tool install -g wix`, plus the UI and Util extensions:
`wix extension add -g WixToolset.UI.wixext/5.0.2 WixToolset.Util.wixext/5.0.2`). Built by package.ps1:
wix build lightcraft.wxs -arch x64 -ext WixToolset.UI.wixext/5.0.2 -ext WixToolset.Util.wixext/5.0.2
-d Version=1.2.3 -d BinDir=... -d IconPath=... -d UiDialogBmp=... -d UiBannerBmp=... -o out.msi
Per-machine install into Program Files (or the folder chosen in the installer, or
`INSTALLFOLDER=...` on the msiexec command line; upgrades keep it), a Start Menu shortcut, `.pcraft` owned by LightCraft,
and LightCraft offered under "Open with" for PSD/PSB and the image formats it reads (without
taking over the user's default app for them).
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package
Name="LightCraft"
Manufacturer="Learning Machines LLC"
Version="$(var.Version)"
UpgradeCode="229BB552-5EFD-479A-BA86-8CB6D926390C"
Scope="perMachine"
Compressed="yes"
Language="1033">
<SummaryInformation Description="LightCraft photo library and raw developer" Manufacturer="Learning Machines LLC" />
<!-- Same-version upgrades let successive pre-releases of one X.Y.Z replace each other
(MSI versions are numeric, so 1.2.3-rc.1 and 1.2.3-rc.2 are both 1.2.3). -->
<MajorUpgrade AllowSameVersionUpgrades="yes"
DowngradeErrorMessage="A newer version of LightCraft is already installed." />
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
<Icon Id="LightcraftIcon.ico" SourceFile="$(var.IconPath)" />
<Property Id="ARPPRODUCTICON" Value="LightcraftIcon.ico" />
<Property Id="ARPURLINFOABOUT" Value="https://getartcraft.com/apps/lightcraft" />
<Property Id="ARPURLUPDATEINFO" Value="https://getartcraft.com/apps/lightcraft" />
<Property Id="ARPHELPLINK" Value="https://discord.gg/artcraft" />
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="INSTALLFOLDER" Name="LightCraft" />
</StandardDirectory>
<!-- The folder of the installed version (written by LightcraftApp), so an upgrade installs
where the user chose instead of moving back to Program Files (issue #399). Searched into
its own property: AppSearch would otherwise overwrite INSTALLFOLDER=... given to msiexec. -->
<Property Id="PREVIOUSINSTALLFOLDER">
<RegistrySearch Id="PreviousInstallFolder" Root="HKLM" Key="Software\LightCraft" Name="InstallFolder" Type="raw" />
</Property>
<SetProperty Id="INSTALLFOLDER" Value="[PREVIOUSINSTALLFOLDER]" After="AppSearch" Sequence="both"
Condition="PREVIOUSINSTALLFOLDER AND NOT INSTALLFOLDER" />
<!-- Older MSIs (including 0.5.0) only recorded the executable in App Paths. Type="file"
without a FileSearch returns its containing directory. Keep the searches separate so
the newer InstallFolder value and an explicit INSTALLFOLDER always take precedence. -->
<Property Id="LEGACYINSTALLFOLDER">
<RegistrySearch Id="LegacyInstallFolder" Root="HKLM"
Key="Software\Microsoft\Windows\CurrentVersion\App Paths\lightcraft.exe" Type="file" />
</Property>
<!-- That directory search doesn't check the executable exists, and when its unquoted path is
gone it falls back to the text before the first space: a deleted
"C:\Program Files\LightCraft\lightcraft.exe" yields "C:\". Only use the folder when this
search, which does require lightcraft.exe, finds it too. -->
<Property Id="LEGACYINSTALLEXE">
<RegistrySearch Id="LegacyInstallExe" Root="HKLM"
Key="Software\Microsoft\Windows\CurrentVersion\App Paths\lightcraft.exe" Type="file">
<FileSearch Id="LegacyInstallExeFile" Name="lightcraft.exe" />
</RegistrySearch>
</Property>
<SetProperty Id="INSTALLFOLDER" Action="SetInstallFolderFromAppPaths" Value="[LEGACYINSTALLFOLDER]"
After="SetINSTALLFOLDER" Sequence="both"
Condition="LEGACYINSTALLFOLDER AND LEGACYINSTALLEXE AND NOT PREVIOUSINSTALLFOLDER AND NOT INSTALLFOLDER" />
<StandardDirectory Id="ProgramMenuFolder" />
<Feature Id="Main" Title="LightCraft" Level="1">
<ComponentGroupRef Id="LightcraftFiles" />
</Feature>
<UIRef Id="LightcraftUI" />
</Package>
<!--
Interactive install UI: Welcome -> Destination Folder -> Ready to install -> progress ->
"Completed" (Finish), with a "Launch LightCraft" checkbox (checked by default). Destination
Folder (issue #399) shows Program Files\LightCraft, or the installed version's folder on an
upgrade; Change... browses, and a folder that can't be used (InvalidDirDlg) is refused before
anything is installed. Failure and cancellation end on WiX's standard FatalError / UserExit
dialogs instead of the completion page. Maintenance (running the MSI again, Repair/Remove from
Apps) uses the standard maintenance dialogs.
This is WixUI_InstallDir without its licence-acceptance page: LightCraft is MIT OR Apache-2.0,
which ask nothing of people who just install and run it, so a click-through EULA would only
add a step. Silent (/qn) and basic-UI (/passive, /qb) installs never show these dialogs, so
they are unaffected, and nothing is launched.
The dialogs, strings and error/cancel pages come from WixToolset.UI.wixext; the side and
banner bitmaps are plain panels generated by package.ps1 (no third-party artwork).
-->
<Fragment>
<WixVariable Id="WixUIDialogBmp" Value="$(var.UiDialogBmp)" />
<WixVariable Id="WixUIBannerBmp" Value="$(var.UiBannerBmp)" />
<Property Id="WIXUI_EXITDIALOGOPTIONALTEXT"
Value="LightCraft has been installed. You can start it at any time from the Start menu." />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch LightCraft" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" />
<!-- "Launch LightCraft" on Finish. WixUnelevatedShellExec (Util extension) asks the desktop
shell (Explorer) to start the program, so it runs as the signed-in user at normal
integrity even when the installer UI itself is elevated (e.g. msiexec started from an
admin prompt); it never runs from the elevated/SYSTEM server side. It is a UI-only control
event, so silent and passive installs never run it. Return="ignore": a failed launch must
not turn a successful install into an error. -->
<Property Id="WixUnelevatedShellExecTarget" Value="[#LightcraftExe]" />
<CustomAction Id="LaunchLightcraft" BinaryRef="Wix4UtilCA_$(sys.BUILDARCHSHORT)"
DllEntry="WixUnelevatedShellExec" Execute="immediate" Impersonate="yes" Return="ignore" />
<UI Id="LightcraftUI">
<TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
<TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
<TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
<Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
<DialogRef Id="ErrorDlg" />
<DialogRef Id="FatalError" />
<DialogRef Id="FilesInUse" />
<DialogRef Id="MsiRMFilesInUse" />
<DialogRef Id="PrepareDlg" />
<DialogRef Id="ProgressDlg" />
<DialogRef Id="ResumeDlg" />
<DialogRef Id="UserExit" />
<DialogRef Id="WelcomeDlg" />
<DialogRef Id="InstallDirDlg" />
<DialogRef Id="BrowseDlg" />
<DialogRef Id="InvalidDirDlg" />
<DialogRef Id="DiskCostDlg" />
<DialogRef Id="VerifyReadyDlg" />
<DialogRef Id="MaintenanceWelcomeDlg" />
<DialogRef Id="MaintenanceTypeDlg" />
<!-- The folder InstallDirDlg edits. -->
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
<!-- First install / upgrade: Welcome -> Destination Folder -> Ready to install. A patch skips
the folder (it can't move an installed product). -->
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Condition="NOT Installed" />
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Condition="Installed AND PATCH" />
<!-- Destination Folder, as in WixUI_InstallDir: Next sets the folder, validates it (WiX's
WixUIValidatePath) and refuses one that can't be used; Change... opens the folder browser,
whose OK validates too. -->
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(sys.BUILDARCHSHORT)" Order="2"
Condition="NOT WIXUI_DONTVALIDATEPATH" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"
Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4"
Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=&quot;1&quot;" />
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2" />
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(sys.BUILDARCHSHORT)" Order="3"
Condition="NOT WIXUI_DONTVALIDATEPATH" />
<Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"
Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1"
Condition="NOT Installed" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="3"
Condition="Installed AND PATCH" />
<!-- Maintenance: Welcome -> Repair / Remove -> Ready. -->
<Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg" />
<Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg" />
<Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg" />
<Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2"
Condition="Installed AND NOT PATCH" />
<!-- Completion page: optionally launch, then close. -->
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchLightcraft" Order="1"
Condition="WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 AND NOT Installed" />
<Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999" />
<!-- One feature, nothing to change: no "Change" in Apps & features. -->
<Property Id="ARPNOMODIFY" Value="1" />
</UI>
<UIRef Id="WixUI_Common" />
</Fragment>
<Fragment>
<ComponentGroup Id="LightcraftFiles" Directory="INSTALLFOLDER">
<Component Id="LightcraftApp" Guid="8A1D3A39-F7C1-47B1-97AE-40C80BF82F4A">
<File Id="LightcraftExe" Source="$(var.BinDir)\lightcraft.exe" KeyPath="yes">
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="LightCraft"
Description="LightCraft photo library and raw developer" WorkingDirectory="INSTALLFOLDER" Advertise="yes"
Icon="LightcraftIcon.ico" IconIndex="0" />
</File>
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\lightcraft.exe" Value="[#LightcraftExe]" Type="string" />
<RegistryValue Root="HKLM" Key="Software\LightCraft" Name="InstallFolder" Value="[INSTALLFOLDER]" Type="string" />
</Component>
<Component Id="LightcraftCli">
<File Id="LightcraftCliExe" Source="$(var.BinDir)\lightcraft-cli.exe" KeyPath="yes" />
</Component>
</ComponentGroup>
</Fragment>
</Wix>