DMG App Porter — macOS → Linux + Windows Conversion Skill
Quick Start
# Full pipeline: DMG → Linux AppImage + Windows .exe installer
dmg=app.dmg; name=MyApp; ver=1.0.0
7z x "$dmg" -odmg_raw && mount_hfs dmg_raw/*.hfs /mnt/dmg
cp -r /mnt/dmg/"$name".app ./app_source
node {baseDir}/scripts/patch-windows.js --input ./app_source --output ./app_win
node {baseDir}/scripts/patch-windows.js --input ./app_source --output ./app_linux --target linux
# Then build launchers + installers per phases 5-8
Prerequisites
| Tool |
Linux |
Windows |
Purpose |
| 7-Zip |
apt install p7zip-full |
choco install 7zip |
DMG extraction |
| hfsutils |
apt install hfsutils hfsprogs |
(7z handles HFS+) |
HFS+ mount |
| Node.js 18+ |
nodesource setup |
nodejs.org |
ASAR ops |
| Rust + cargo |
rustup.rs |
rustup.rs |
Windows launcher |
| cargo-zigbuild |
cargo install cargo-zigbuild |
N/A |
Cross-compile |
| NSIS |
apt install nsis |
choco install nsis |
Windows installer |
| appimagetool |
download from AppImage repo |
N/A |
Linux packaging |
Phase 1: DMG Extraction
Step 1A: Extract with 7-Zip
7z x app.dmg -odmg_extracted -y
# Output: dmg_extracted/4.Main_Data_partition.hfs (or similar)
Step 1B: Mount HFS+ Partition (Linux)
# Find the HFS+ partition image
HFS_IMG=$(find dmg_extracted -name '*.hfs' -o -name '*partition*' | head -1)
sudo mkdir -p /mnt/dmg
sudo mount -t hfsplus -o loop,ro "$HFS_IMG" /mnt/dmg
# If read-only fails: sudo mount -t hfs -o loop,ro "$HFS_IMG" /mnt/dmg
Step 1C: Alternative — hfsutils
hmount dmg_extracted/*.hfs
hls -l # List root
hcd "MyApp.app" # Enter .app bundle
hcd Contents/MacOS
hcopy MyAppExecutable ~/output/ # Copy files
humount
Decision Point
If 7z lists .hfs file → use hfsplus mount (Step 1B)
If 7z directly extracts .app → skip mount, use extracted dir
If encrypted DMG → requires dmg2img first: dmg2img app.dmg app.img
Common Failures
| Error |
Cause |
Fix |
Invalid or corrupted archive |
APFS-only DMG |
Use dmg2img then mount |
mount: unknown filesystem type 'hfsplus' |
Missing kernel module |
sudo modprobe hfsplus then apt install hfsprogs |
Permission denied |
SIP on macOS |
Extract on Linux VM/container |
Phase 2: App Analysis
Step 2A: Identify App Type
APP_DIR="./app_source/MyApp.app/Contents"
# Check for Electron
test -f "$APP_DIR/Resources/app.asar" && echo "ELECTRON" || echo "NATIVE"
test -d "$APP_DIR/Frameworks" && echo "HAS_FRAMEWORKS" || echo "NO_FRAMEWORKS"
# Parse Info.plist
/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "$APP_DIR/Info.plist" 2>/dev/null \
|| python3 -c "import plistlib; p=plistlib.load(open('$APP_DIR/Info.plist','rb')); print(p.get('CFBundleIdentifier','unknown'))"
Decision Tree
app.asar exists?
├─ YES → Electron app (Phase 3)
│ └─ app.asar.unpacked exists?
│ ├─ YES → Has native modules, check compatibility
│ └─ NO → Pure JS, straightforward patching
└─ NO → Native Cocoa app
└─ Requires full rewrite or Wine wrapping (out of scope, see Phase 4 mapping only)
Step 2B: Extract Metadata
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "$APP_DIR/Info.plist")
APP_NAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleName" "$APP_DIR/Info.plist")
APP_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$APP_DIR/Info.plist")
MIN_MACOS=$(/usr/libexec/PlistBuddy -c "Print LSMinimumSystemVersion" "$APP_DIR/Info.plist")
ELECTRON_VER=$(cat "$APP_DIR/Frameworks/Electron Framework.framework/Versions/A/Info.plist" \
| /usr/libexec/PlistBuddy -c "Print CFBundleVersion" /dev/stdin 2>/dev/null || echo "unknown")
Phase 3: Electron App Conversion (ASAR Patching)
Run the automated patcher or apply manually. See {baseDir}/references/asar-patching-guide.md for full details.
# Automated (recommended)
node {baseDir}/scripts/patch-windows.js --input ./app_source --output ./app_win --target win32
# Manual extraction + inspection
npx asar extract "$APP_DIR/Resources/app.asar" ./app_unpacked/
# Apply 7-category patches (see below), then:
npx asar pack ./app_patched/ ./app_win/Resources/app.asar
7-Category Patch Summary
| # |
Category |
What Changes |
Critical Pattern |
| 1 |
Window mgmt |
titleBarStyle:'hiddenInset' → frame:true |
titleBarStyle\s*[:=]\s*['"]hiddenInset['"] |
| 2 |
Paths |
macOS-specific → process.platform guards |
`'/Users/ |
| 3 |
Single instance |
Unix socket → Named Mutex (Win) / flock (Linux) |
`.sock['")] |
| 4 |
Tray |
.png → .ico on Windows |
`tray.set.*Icon |
| 5 |
Auto-updater |
Squirrel.Mac → Squirrel.Windows/AppImage |
`autoUpdater |
| 6 |
Desktop names |
CFBundleIdentifier → platform IDs |
`CFBundleIdentifier |
| 7 |
Quit guard |
beforeunload preventDefault → platform shutdown |
beforeunload.*preventDefault |
Phase 4: Platform Equivalent Mapping
Full table in {baseDir}/references/platform-equivalents.md. Key mappings:
| macOS |
Linux |
Windows |
Shell (/bin/bash) |
Bash |
PowerShell (pwsh.exe) |
Unix socket (.sock) |
Unix socket / D-Bus |
Named Pipe (\\.\pipe\) |
prctl(PR_SET_PDEATHSIG) |
prctl |
Windows Job Object |
.desktop file |
.desktop + D-Bus |
Registry HKCR + URL protocol |
| Polkit |
Polkit / pkexec |
UAC (ShellExecute runas) |
| launchd |
systemd user unit |
Task Scheduler / Registry Run |
| Keychain |
libsecret |
Credential Manager |
Phase 5: Windows Launcher (Rust)
Full guide in {baseDir}/references/rust-launcher-guide.md. Summary:
# Setup
rustup target add x86_64-pc-windows-gnu
cargo install cargo-zigbuild
# Build
cd launcher-win && cargo zigbuild --target x86_64-pc-windows-gnu --release
# Output: target/x86_64-pc-windows-gnu/release/myapp-launcher.exe (~2.2MB)
Key Implementation Points
- Single Instance:
CreateMutexW(None, FALSE, "Global\\MyApp-SingleInstance") — check GetLastError() == ERROR_ALREADY_EXISTS
- Process Lifecycle:
CreateJobObjectW + AssignProcessToJobObject + SetInformationJobObject(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)
- Port Allocation: Bind
127.0.0.1:0, read assigned port, pass to browser
- TLS: Use
rustls-tls feature (NOT native-tls) to avoid Windows SDK dependency
Phase 6: Linux Launcher (Shell)
#!/usr/bin/env bash
APPDIR="$(cd "$(dirname "$0")" && pwd)"
export PATH="$APPDIR:$PATH"
export ELECTRON_RUN_AS_NODE=1
# Single instance via flock
LOCK_FILE="/tmp/${APP_NAME}.lock"
exec 9>"$LOCK_FILE"
flock -n 9 || { echo "Already running"; exit 0; }
# D-Bus activation (optional)
dbus-send --session --dest=org.freedesktop.DBus --type=method_call \
/org/freedesktop/DBus org.freedesktop.DBus.RequestName string:"com.example.$APP_NAME" 2>/dev/null
# Launch
exec "$APPDIR/electron" "$APPDIR/resources/app.asar" "$@"
XDG .desktop File
[Desktop Entry]
Name=MyApp
Comment=My Application
Exec=/opt/myapp/launcher.sh %U
Icon=myapp
Type=Application
Categories=Development;
MimeType=x-scheme-handler/myapp;
StartupNotify=true
systemd User Unit
[Unit]
Description=MyApp
After=graphical-session.target
[Service]
Type=simple
ExecStart=/opt/myapp/launcher.sh
Restart=on-failure
[Install]
WantedBy=default.target
Phase 7: NSIS Installer (Windows)
Full guide in {baseDir}/references/nsis-installer-guide.md.
!include "MUI2.nsh"
Name "MyApp"
OutFile "MyApp-Setup-1.0.0.exe"
InstallDir "$PROGRAMFILES64\MyApp"
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"
Section "MainSection" SEC01
SetOutPath "$INSTDIR"
File /r "dist\win\*.*"
CreateShortCut "$DESKTOP\MyApp.lnk" "$INSTDIR\MyApp.exe"
CreateShortCut "$SMPROGRAMS\MyApp.lnk" "$INSTDIR\MyApp.exe"
SectionEnd
Section "URLProtocol"
WriteRegStr HKCR "myapp" "" "URL:MyApp Protocol"
WriteRegStr HKCR "myapp" "URL Protocol" ""
WriteRegStr HKCR "myapp\shell\open\command" "" '"$INSTDIR\MyApp.exe" "%1"'
SectionEnd
Section "Uninstall"
Delete "$INSTDIR\*.*"
RMDir /r "$INSTDIR"
Delete "$DESKTOP\MyApp.lnk"
Delete "$SMPROGRAMS\MyApp.lnk"
DeleteRegKey HKCR "myapp"
SectionEnd
Phase 8: AppImage / dpkg (Linux)
AppImage
mkdir -p AppDir/usr/bin AppDir/usr/lib AppDir/usr/share/applications AppDir/usr/share/icons/hicolor/256x256/apps
cp -r dist/linux/* AppDir/usr/bin/
cp icons/myapp-256.png AppDir/usr/share/icons/hicolor/256x256/apps/myapp.png
cat > AppDir/myapp.desktop << 'EOF'
[Desktop Entry]
Name=MyApp;Exec=myapp;Icon=myapp;Type=Application;Categories=Development;
EOF
cat > AppDir/AppRun << 'SCRIPT'
#!/bin/bash
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
exec "$HERE/usr/bin/launcher.sh" "$@"
SCRIPT
chmod +x AppDir/AppRun
appimagetool AppDir MyApp-1.0.0-x86_64.AppImage
dpkg
mkdir -p pkg/DEBIAN pkg/usr/bin pkg/usr/lib/myapp pkg/usr/share/applications pkg/usr/share/icons
# control file
cat > pkg/DEBIAN/control << EOF
Package: myapp
Version: 1.0.0
Architecture: amd64
Maintainer: dev@example.com
Description: My Application
Depends: libgtk-3-0, libnotify4, libnss3, libxss1
EOF
cp -r dist/linux/* pkg/usr/lib/myapp/
dpkg-deb --build pkg myapp_1.0.0_amd64.deb
Phase 9: Testing & Validation
Binary Verification
# Windows launcher
file target/x86_64-pc-windows-gnu/release/myapp-launcher.exe
# Expected: PE32+ executable (console) x86-64
# ASAR integrity
sha256sum app_source/Resources/app.asar > original.sha256
sha256sum app_win/Resources/app.asar > patched.sha256
# They SHOULD differ (content was patched), but verify structure:
npx asar list app_win/Resources/app.asar | wc -l # Should match original count
Platform-Specific Tests
# Windows (in VM or Wine)
wine myapp-launcher.exe --version # Should not crash
# Linux
./MyApp-1.0.0-x86_64.AppImage --version # Should launch
xdg-mime default myapp.desktop x-scheme-handler/myapp # URL protocol
Validation Checklist
- App launches on both platforms
- Single instance enforcement works
- URL protocol (myapp://) triggers app
- Tray icon appears with correct format
- Auto-updater points to correct platform feed
- Window controls (min/max/close) work
- File dialogs show correct filters
- App data stored in correct platform path
- Uninstaller removes all files and registry entries (Windows)
- .desktop file validates:
desktop-file-validate myapp.desktop
Error Handling
| Phase |
Error |
Cause |
Fix |
| 1 |
dmg2img: no such file |
Missing tool |
apt install dmg2img |
| 1 |
hfsplus mount fails |
Kernel module missing |
modprobe hfsplus; apt install hfsprogs |
| 2 |
plistlib parse error |
Binary plist |
plutil -convert xml1 Info.plist first |
| 3 |
asar extract ENOENT |
Corrupt asar |
Re-extract from DMG |
| 3 |
patch regex no match |
Obfuscated code |
Search for functional equivalents |
| 5 |
native-tls link error |
Windows SDK missing |
Switch to rustls-tls in Cargo.toml |
| 5 |
zigbuild target error |
Target not added |
rustup target add x86_64-pc-windows-gnu |
| 7 |
NSIS compile error |
Bad path with spaces |
Quote all paths, avoid spaces in INSTDIR |
| 8 |
AppImage FUSE error |
Missing FUSE |
apt install libfuse2 or --appimage-extract |
Security Considerations
- Never embed secrets in ASAR — it's plaintext, extractable with
npx asar extract
- Sign Windows binaries with Authenticode (signtool) or SmartScreen will warn users
- Verify Electron version — old versions have known CVEs; update if < latest stable
- Lock mutex name — use
Global\Vendor-AppName-Hash to prevent squatting
- Sandbox the server — launcher should drop privileges after port allocation
- HTTPS only — for auto-updater feeds, pin certificates if possible
- Review native modules —
.node binaries in app.asar.unpacked must be recompiled for target OS