feat(packaging): open files from argv, ship Windows MSI installer (#36)

Cross-platform prerequisite for OS-level file associations: the boot
task now consumes `std::env::args_os().nth(1)` as a path and dispatches
`Message::OpenRecent` for it. Flag-style args (starting with `-`) are
ignored, and `OpenRecent` already does the file-existence check, so a
bogus path lands as a clean command-line error instead of a panic.

Linux: the AppImage's desktop entry adds `image/vnd.dwg` alongside the
existing `image/vnd.dxf` MIME type and grows a `%f` placeholder in
`Exec=` so the launcher forwards the picked file path. Once the
AppImage is integrated with the system (xdg-mime / AppImageLauncher),
double-clicking a .dwg / .dxf opens it in Open CAD Studio.

Windows: new `packaging/windows/main.wxs` defines an MSI installer —
per-machine install to `Program Files\Open CAD Studio`, Start Menu
shortcut, `MajorUpgrade` so newer MSIs replace older ones, and a
single ProgID `OpenCADStudio.Drawing` that owns both `.dwg` and
`.dxf` and launches the exe with the file as `argv[1]`. The icon is
pulled from the SVG logo, converted to multi-resolution ICO at build
time with the runner's pre-installed ImageMagick. CI now also runs
`candle` / `light` (WiX Toolset 3, pre-installed on `windows-latest`)
and uploads both the bare `.exe` (portable) and `.msi` (installer)
to the release.

macOS Info.plist already declares the DWG / DXF UTIs; combined with
the argv-handling change above, double-clicking a drawing in Finder
will open it in Open CAD Studio without further packaging changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-29 23:34:14 +03:00
commit a0800d95c9
4 changed files with 180 additions and 7 deletions

View file

@ -78,13 +78,53 @@ jobs:
- name: Build
run: cargo build --release
- name: Upload exe to release
- name: Convert SVG icon to ICO
shell: pwsh
run: |
# ImageMagick is pre-installed on windows-latest. The
# auto-resize generates every size Explorer / Add-Remove
# Programs / the Start Menu shortcut may request from a
# single source SVG.
magick assets/logo.svg `
-define icon:auto-resize=16,24,32,48,64,128,256 `
packaging/windows/AppIcon.ico
- name: Build MSI installer
shell: pwsh
run: |
$version = "${{ github.ref_name }}" -replace '^v', ''
if (-not $version) { $version = "0.0.0" }
# WiX Toolset 3.x is pre-installed on windows-latest; $env:WIX
# points to the install root. candle compiles .wxs → .wixobj
# and light links it into the final .msi.
$candle = Join-Path $env:WIX "bin\candle.exe"
$light = Join-Path $env:WIX "bin\light.exe"
& $candle -arch x64 `
-dVersion=$version `
-dSource=target\release\OpenCADStudio.exe `
-dIcon=packaging\windows\AppIcon.ico `
packaging\windows\main.wxs `
-out packaging\windows\main.wixobj
if ($LASTEXITCODE -ne 0) { throw "candle failed" }
& $light packaging\windows\main.wixobj -out OpenCADStudio.msi
if ($LASTEXITCODE -ne 0) { throw "light failed" }
- name: Upload artifacts to release
if: github.event_name == 'release'
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv target/release/OpenCADStudio.exe OpenCADStudio-${{ github.ref_name }}-windows-x86_64.exe
gh release upload ${{ github.ref_name }} OpenCADStudio-${{ github.ref_name }}-windows-x86_64.exe --clobber --repo ${{ github.repository }}
$tag = "${{ github.ref_name }}"
# Ship both the bare .exe (portable, no install) and the
# .msi (installer with Start Menu shortcut + .dwg / .dxf
# file association). Users pick the form they want.
Move-Item target\release\OpenCADStudio.exe "OpenCADStudio-$tag-windows-x86_64.exe"
Move-Item OpenCADStudio.msi "OpenCADStudio-$tag-windows-x86_64.msi"
gh release upload $tag `
"OpenCADStudio-$tag-windows-x86_64.exe" `
"OpenCADStudio-$tag-windows-x86_64.msi" `
--clobber --repo ${{ github.repository }}
build-macos:
runs-on: macos-14 # Apple Silicon (arm64) only

View file

@ -1,11 +1,11 @@
[Desktop Entry]
Name=Open CAD Studio
Comment=A CAD application for 2D/3D drawing and design
Exec=OpenCADStudio
Exec=OpenCADStudio %f
Icon=io.github.HakanSeven12.OpenCadStudio
Terminal=false
Type=Application
Categories=Graphics;Engineering;
MimeType=image/vnd.dxf;
Keywords=CAD;DXF;drawing;design;engineering;2D;3D;
MimeType=image/vnd.dwg;image/vnd.dxf;
Keywords=CAD;DWG;DXF;drawing;design;engineering;2D;3D;
StartupWMClass=OpenCADStudio

118
packaging/windows/main.wxs Normal file
View file

@ -0,0 +1,118 @@
<?xml version='1.0' encoding='windows-1252'?>
<!--
WiX source for the Windows MSI installer.
Build (CI / locally with WiX Toolset 3.x in PATH):
candle.exe -arch x64 -dVersion=<x.y.z> -dSource=target\release\OpenCADStudio.exe \
-dIcon=packaging\windows\AppIcon.ico packaging\windows\main.wxs \
-out packaging\windows\main.wixobj
light.exe packaging\windows\main.wixobj -out OpenCADStudio.msi
Provides:
* Per-machine install under Program Files\Open CAD Studio.
* Start Menu shortcut under Programs\Open CAD Studio.
* File associations for .dwg and .dxf via a single ProgID; both
route through `OpenCADStudio.exe "%1"` so the OS hands the file
path to the app as argv[1] (consumed in OpenCADStudio::boot).
* Standard Add/Remove Programs entry with icon.
* MajorUpgrade — installing a newer MSI replaces the old version.
-->
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
<Product
Id='*'
Name='Open CAD Studio'
UpgradeCode='bd6bceab-6c7a-4ce2-a62d-6ef06a92f8b1'
Manufacturer='Hakan Seven'
Language='1033'
Codepage='1252'
Version='$(var.Version)'>
<Package
Id='*'
Keywords='Installer'
Description='Open CAD Studio Installer'
Manufacturer='Hakan Seven'
InstallerVersion='450'
Languages='1033'
Compressed='yes'
InstallScope='perMachine'
SummaryCodepage='1252' />
<MajorUpgrade
Schedule='afterInstallInitialize'
DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.' />
<Media Id='1' Cabinet='product.cab' EmbedCab='yes' />
<Icon Id='AppIcon.ico' SourceFile='$(var.Icon)' />
<Property Id='ARPPRODUCTICON' Value='AppIcon.ico' />
<Property Id='ARPHELPLINK' Value='https://github.com/HakanSeven12/OpenCADStudio' />
<Property Id='ARPURLINFOABOUT' Value='https://github.com/HakanSeven12/OpenCADStudio' />
<Directory Id='TARGETDIR' Name='SourceDir'>
<Directory Id='ProgramFiles64Folder'>
<Directory Id='APPLICATIONROOTDIRECTORY' Name='Open CAD Studio'>
<Component Id='MainExecutable' Guid='2900c116-e9a5-4650-8a4c-48c0987d1a33' Win64='yes'>
<File
Id='OpenCADStudioEXE'
Name='OpenCADStudio.exe'
DiskId='1'
Source='$(var.Source)'
KeyPath='yes'>
<Shortcut
Id='StartMenuShortcut'
Directory='ProgramMenuDir'
Name='Open CAD Studio'
Description='Launch Open CAD Studio'
WorkingDirectory='APPLICATIONROOTDIRECTORY'
Icon='AppIcon.ico'
IconIndex='0'
Advertise='yes' />
</File>
<!--
File association: a single ProgID handles both .dwg and
.dxf. ContentType isn't strictly required on Windows but
improves "Open with…" suggestions.
-->
<ProgId
Id='OpenCADStudio.Drawing'
Description='Drawing'
Icon='OpenCADStudioEXE'
IconIndex='0'>
<Extension Id='dwg' ContentType='image/vnd.dwg'>
<Verb Id='open' Command='Open' TargetFile='OpenCADStudioEXE' Argument='"%1"' />
</Extension>
<Extension Id='dxf' ContentType='image/vnd.dxf'>
<Verb Id='open' Command='Open' TargetFile='OpenCADStudioEXE' Argument='"%1"' />
</Extension>
</ProgId>
</Component>
</Directory>
</Directory>
<Directory Id='ProgramMenuFolder'>
<Directory Id='ProgramMenuDir' Name='Open CAD Studio'>
<Component Id='ProgramMenuDirComp' Guid='34a788ba-f9d4-4e73-b1ff-9eb8749b369e'>
<RemoveFolder Id='ProgramMenuDir' On='uninstall' />
<RegistryValue
Root='HKCU'
Key='Software\Open CAD Studio'
Name='installed'
Type='integer'
Value='1'
KeyPath='yes' />
</Component>
</Directory>
</Directory>
</Directory>
<Feature Id='Complete' Title='Open CAD Studio' Level='1'>
<ComponentRef Id='MainExecutable' />
<ComponentRef Id='ProgramMenuDirComp' />
</Feature>
</Product>
</Wix>

View file

@ -1036,7 +1036,22 @@ impl OpenCADStudio {
Message::UpdateCheckResult,
);
let focus_cmd = s.focus_cmd_input();
(s, Task::batch([open_main, check_update, focus_cmd]))
// Open a file path passed on the command line. Used by the
// OS file association: double-clicking a .dwg in the file
// explorer launches the binary with the file path as the
// first positional argument. Unknown / flag-style args are
// ignored. `Message::OpenRecent` does the existence check
// and reports a clean error if the path is bogus.
let cli_open: Task<Message> = std::env::args_os()
.nth(1)
.map(PathBuf::from)
.filter(|p| !p.as_os_str().to_string_lossy().starts_with('-'))
.map(|p| Task::done(Message::OpenRecent(p)))
.unwrap_or_else(Task::none);
(
s,
Task::batch([open_main, check_update, focus_cmd, cli_open]),
)
}
}