markdown · 9519 bytes Raw Blame History

Sniffly macOS Packaging Walkthrough

This is a step-by-step guide to package Sniffly for macOS distribution.

Prerequisites

Before starting, make sure you have:

  • ✅ Sniffly built successfully (./build/sniffly exists)
  • dylibbundler installed: brew install dylibbundler
  • ✅ An application icon (optional, but recommended)
  • 📝 Apple Developer account (only needed for code signing)

Version Management

The version is controlled by a single VERSION file at the project root. To release a new version:

echo "0.3.0" > VERSION
meson setup build --reconfigure
meson compile -C build

The version automatically appears in:

  • The binary output (./build/sniffly --version)
  • Info.plist in the app bundle
  • The DMG filename

Quick Start: Automated Packaging

# One command to create the .app and .dmg
./scripts/package-macos.sh

This creates:

  • Sniffly.app - The application bundle
  • Sniffly-0.2.0-macOS.dmg - Distributable installer

What The Script Does

  1. Reads version from VERSION file
  2. Creates .app bundle structure
  3. Copies the executable
  4. Creates Info.plist with version info
  5. Bundles GTK4 libraries using dylibbundler
  6. Creates a distributable DMG

Step-by-Step Manual Process

If you want to understand what's happening or customize the process:

Step 1: Build the Release Binary

# Clean build for release
rm -rf build
meson setup build --buildtype=release
meson compile -C build

# Verify it works
./build/sniffly --version

Step 2: Create the App Bundle Structure

mkdir -p Sniffly.app/Contents/MacOS
mkdir -p Sniffly.app/Contents/Resources
mkdir -p Sniffly.app/Contents/Frameworks

Step 3: Copy the Executable

cp build/sniffly Sniffly.app/Contents/MacOS/sniffly
chmod +x Sniffly.app/Contents/MacOS/sniffly

Step 4: Create Info.plist

cat > Sniffly.app/Contents/Info.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleExecutable</key>
    <string>sniffly</string>
    <key>CFBundleIdentifier</key>
    <string>org.fortrangoingonforty.sniffly</string>
    <key>CFBundleName</key>
    <string>Sniffly</string>
    <key>CFBundleDisplayName</key>
    <string>Sniffly</string>
    <key>CFBundleVersion</key>
    <string>0.2.0</string>
    <key>CFBundleShortVersionString</key>
    <string>0.2.0</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleIconFile</key>
    <string>sniffly</string>
    <key>LSMinimumSystemVersion</key>
    <string>11.0</string>
    <key>LSApplicationCategoryType</key>
    <string>public.app-category.utilities</string>
</dict>
</plist>
EOF

You need a .icns file. If you have a 1024x1024 PNG:

# Create iconset directory
mkdir sniffly.iconset

# Generate all sizes
sips -z 16 16     icon-1024.png --out sniffly.iconset/icon_16x16.png
sips -z 32 32     icon-1024.png --out sniffly.iconset/icon_16x16@2x.png
sips -z 32 32     icon-1024.png --out sniffly.iconset/icon_32x32.png
sips -z 64 64     icon-1024.png --out sniffly.iconset/icon_32x32@2x.png
sips -z 128 128   icon-1024.png --out sniffly.iconset/icon_128x128.png
sips -z 256 256   icon-1024.png --out sniffly.iconset/icon_128x128@2x.png
sips -z 256 256   icon-1024.png --out sniffly.iconset/icon_256x256.png
sips -z 512 512   icon-1024.png --out sniffly.iconset/icon_256x256@2x.png
sips -z 512 512   icon-1024.png --out sniffly.iconset/icon_512x512.png
sips -z 1024 1024 icon-1024.png --out sniffly.iconset/icon_512x512@2x.png

# Convert to .icns
iconutil -c icns sniffly.iconset -o Sniffly.app/Contents/Resources/sniffly.icns

# Cleanup
rm -rf sniffly.iconset

Step 6: Bundle GTK4 Libraries

This is the crucial step that makes your app "just work" on any Mac:

dylibbundler -od -b \
    -x Sniffly.app/Contents/MacOS/sniffly \
    -d Sniffly.app/Contents/Frameworks/ \
    -p @executable_path/../Frameworks/

What this does:

  • Finds all dylib dependencies (GTK4, Cairo, GLib, etc.)
  • Copies them into Frameworks/
  • Rewrites the executable to look in @executable_path/../Frameworks/
  • Makes the app fully self-contained

Expected size: ~100-150MB (GTK4 is big!)

Step 7: Test the App Bundle

open Sniffly.app

If it works, you're ready to create the DMG!

Step 8: Create the DMG

# Create temporary folder
mkdir dmg_temp
cp -r Sniffly.app dmg_temp/

# Add Applications folder shortcut
ln -s /Applications dmg_temp/Applications

# Create DMG
hdiutil create -volname "Sniffly" \
    -srcfolder dmg_temp \
    -ov -format UDZO \
    Sniffly-0.2.0-macOS.dmg

# Cleanup
rm -rf dmg_temp

If you have an Apple Developer account:

# Sign the app
codesign --deep --force --verify --verbose \
    --sign "Developer ID Application: Your Name (TEAM_ID)" \
    Sniffly.app

# Verify signature
codesign -dv Sniffly.app

# Create DMG from signed app
# (repeat Step 8 after signing)

# Notarize (required for distribution outside App Store)
xcrun notarytool submit Sniffly-0.2.0-macOS.dmg \
    --apple-id "your@email.com" \
    --password "app-specific-password" \
    --team-id "YOUR_TEAM_ID" \
    --wait

# Staple notarization ticket
xcrun stapler staple Sniffly-0.2.0-macOS.dmg

Why sign and notarize?

  • Without: Users see "unidentified developer" warning
  • With: Clean installation experience, no warnings

Cost: $99/year for Apple Developer account

Distribution

Once you have Sniffly-0.2.0-macOS.dmg:

  1. Test on a clean Mac (or VM) without development tools installed
  2. Upload to your server:
    scp Sniffly-0.2.0-macOS.dmg user@yourserver.com:/var/www/downloads/
    
  3. Create a download page with:
    • Link to the DMG
    • System requirements (macOS 11.0+)
    • Installation instructions
    • Screenshot/demo

Example Download Page

<!DOCTYPE html>
<html>
<head>
    <title>Download Sniffly</title>
</head>
<body>
    <h1>Download Sniffly v0.2.0</h1>
    <p>A fast, visual disk space analyzer for macOS</p>

    <a href="Sniffly-0.2.0-macOS.dmg" class="download-button">
        Download for macOS (150 MB)
    </a>

    <h2>Requirements</h2>
    <ul>
        <li>macOS 11.0 (Big Sur) or later</li>
        <li>Apple Silicon or Intel processor</li>
    </ul>

    <h2>Installation</h2>
    <ol>
        <li>Download Sniffly-0.2.0-macOS.dmg</li>
        <li>Open the DMG file</li>
        <li>Drag Sniffly to Applications folder</li>
        <li>Launch from Applications</li>
    </ol>
</body>
</html>

Release Checklist

Before releasing a new version:

  • Update VERSION file
  • Test on clean macOS installation
  • Verify --version shows correct version
  • Test basic functionality (scan, navigation, etc.)
  • Build release binary (--buildtype=release)
  • Bundle GTK4 libraries with dylibbundler
  • Test app bundle works standalone
  • Create DMG
  • (Optional) Code sign and notarize
  • Upload to distribution server
  • Update download page
  • Announce release (social media, website, etc.)

Troubleshooting

"Sniffly is damaged and can't be opened"

This happens when macOS quarantines unsigned apps. Users can bypass with:

xattr -cr /Applications/Sniffly.app

Or you need to code sign and notarize the app.

App crashes immediately

  • Check if GTK4 libraries are bundled: ls Sniffly.app/Contents/Frameworks/
  • Should see many .dylib files
  • Verify with: otool -L Sniffly.app/Contents/MacOS/sniffly
    • Should show @executable_path/../Frameworks/ paths

DMG too large (>200MB)

Normal! GTK4 is big. Options:

  • Accept it (users download once)
  • Require system GTK4 (smaller DMG, but users must brew install gtk4)

Can't sign the app

You need an Apple Developer account ($99/year). For free distribution:

  • Skip signing (users will see warning)
  • Document how to bypass Gatekeeper

Version Number Advice

You asked about version numbering. Here's my take:

Current State of Sniffly (0.2.0 suggestion):

  • ✅ Core functionality works (scanning, treemap, navigation)
  • ✅ Progressive scanning is solid
  • ✅ 3D effects, navigation locking
  • ⚠️ Some rough edges (had bugs we fixed)
  • ⚠️ No installers yet (this guide creates them)
  • ❌ Not feature-complete vs original SpaceSniffer

Version recommendations:

  • 0.1.0 - Tech preview, barely works
  • 0.2.0 - Alpha release (current state) ← I recommend this
  • 0.5.0 - Beta release (feature complete, but bugs)
  • 0.9.0 - Release candidate
  • 1.0.0 - Stable, production-ready

Why 0.2.0?

  • Shows progress from initial experiments
  • Sets expectations (alpha quality)
  • Room to grow before 1.0
  • Conservative (you prefer this)

Semantic versioning for future:

  • 0.x.y - Pre-1.0 development
  • 1.0.0 - First stable release
  • 1.1.0 - Add features (backwards compatible)
  • 1.0.1 - Bug fixes
  • 2.0.0 - Breaking changes

Next Steps

After successful macOS packaging, consider:

  • Linux AppImage (simpler than .deb/.rpm)
  • Homebrew cask formula (brew install --cask sniffly)
  • Automatic updates (Sparkle framework)
  • Analytics (crash reporting, usage stats)
  • Website with documentation

Questions? The packaging script at scripts/package-macos.sh automates all of this!

View source
1 # Sniffly macOS Packaging Walkthrough
2
3 This is a step-by-step guide to package Sniffly for macOS distribution.
4
5 ## Prerequisites
6
7 Before starting, make sure you have:
8 - ✅ Sniffly built successfully (`./build/sniffly` exists)
9 -`dylibbundler` installed: `brew install dylibbundler`
10 - ✅ An application icon (optional, but recommended)
11 - 📝 Apple Developer account (only needed for code signing)
12
13 ## Version Management
14
15 The version is controlled by a single `VERSION` file at the project root. To release a new version:
16
17 ```bash
18 echo "0.3.0" > VERSION
19 meson setup build --reconfigure
20 meson compile -C build
21 ```
22
23 The version automatically appears in:
24 - The binary output (`./build/sniffly --version`)
25 - Info.plist in the app bundle
26 - The DMG filename
27
28 ## Quick Start: Automated Packaging
29
30 ```bash
31 # One command to create the .app and .dmg
32 ./scripts/package-macos.sh
33 ```
34
35 This creates:
36 - `Sniffly.app` - The application bundle
37 - `Sniffly-0.2.0-macOS.dmg` - Distributable installer
38
39 ### What The Script Does
40
41 1. Reads version from `VERSION` file
42 2. Creates `.app` bundle structure
43 3. Copies the executable
44 4. Creates `Info.plist` with version info
45 5. Bundles GTK4 libraries using `dylibbundler`
46 6. Creates a distributable DMG
47
48 ## Step-by-Step Manual Process
49
50 If you want to understand what's happening or customize the process:
51
52 ### Step 1: Build the Release Binary
53
54 ```bash
55 # Clean build for release
56 rm -rf build
57 meson setup build --buildtype=release
58 meson compile -C build
59
60 # Verify it works
61 ./build/sniffly --version
62 ```
63
64 ### Step 2: Create the App Bundle Structure
65
66 ```bash
67 mkdir -p Sniffly.app/Contents/MacOS
68 mkdir -p Sniffly.app/Contents/Resources
69 mkdir -p Sniffly.app/Contents/Frameworks
70 ```
71
72 ### Step 3: Copy the Executable
73
74 ```bash
75 cp build/sniffly Sniffly.app/Contents/MacOS/sniffly
76 chmod +x Sniffly.app/Contents/MacOS/sniffly
77 ```
78
79 ### Step 4: Create Info.plist
80
81 ```bash
82 cat > Sniffly.app/Contents/Info.plist << 'EOF'
83 <?xml version="1.0" encoding="UTF-8"?>
84 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
85 <plist version="1.0">
86 <dict>
87 <key>CFBundleExecutable</key>
88 <string>sniffly</string>
89 <key>CFBundleIdentifier</key>
90 <string>org.fortrangoingonforty.sniffly</string>
91 <key>CFBundleName</key>
92 <string>Sniffly</string>
93 <key>CFBundleDisplayName</key>
94 <string>Sniffly</string>
95 <key>CFBundleVersion</key>
96 <string>0.2.0</string>
97 <key>CFBundleShortVersionString</key>
98 <string>0.2.0</string>
99 <key>CFBundlePackageType</key>
100 <string>APPL</string>
101 <key>CFBundleIconFile</key>
102 <string>sniffly</string>
103 <key>LSMinimumSystemVersion</key>
104 <string>11.0</string>
105 <key>LSApplicationCategoryType</key>
106 <string>public.app-category.utilities</string>
107 </dict>
108 </plist>
109 EOF
110 ```
111
112 ### Step 5: Add an Icon (Optional but Recommended)
113
114 You need a `.icns` file. If you have a 1024x1024 PNG:
115
116 ```bash
117 # Create iconset directory
118 mkdir sniffly.iconset
119
120 # Generate all sizes
121 sips -z 16 16 icon-1024.png --out sniffly.iconset/icon_16x16.png
122 sips -z 32 32 icon-1024.png --out sniffly.iconset/icon_16x16@2x.png
123 sips -z 32 32 icon-1024.png --out sniffly.iconset/icon_32x32.png
124 sips -z 64 64 icon-1024.png --out sniffly.iconset/icon_32x32@2x.png
125 sips -z 128 128 icon-1024.png --out sniffly.iconset/icon_128x128.png
126 sips -z 256 256 icon-1024.png --out sniffly.iconset/icon_128x128@2x.png
127 sips -z 256 256 icon-1024.png --out sniffly.iconset/icon_256x256.png
128 sips -z 512 512 icon-1024.png --out sniffly.iconset/icon_256x256@2x.png
129 sips -z 512 512 icon-1024.png --out sniffly.iconset/icon_512x512.png
130 sips -z 1024 1024 icon-1024.png --out sniffly.iconset/icon_512x512@2x.png
131
132 # Convert to .icns
133 iconutil -c icns sniffly.iconset -o Sniffly.app/Contents/Resources/sniffly.icns
134
135 # Cleanup
136 rm -rf sniffly.iconset
137 ```
138
139 ### Step 6: Bundle GTK4 Libraries
140
141 This is the crucial step that makes your app "just work" on any Mac:
142
143 ```bash
144 dylibbundler -od -b \
145 -x Sniffly.app/Contents/MacOS/sniffly \
146 -d Sniffly.app/Contents/Frameworks/ \
147 -p @executable_path/../Frameworks/
148 ```
149
150 What this does:
151 - Finds all dylib dependencies (GTK4, Cairo, GLib, etc.)
152 - Copies them into `Frameworks/`
153 - Rewrites the executable to look in `@executable_path/../Frameworks/`
154 - Makes the app fully self-contained
155
156 **Expected size**: ~100-150MB (GTK4 is big!)
157
158 ### Step 7: Test the App Bundle
159
160 ```bash
161 open Sniffly.app
162 ```
163
164 If it works, you're ready to create the DMG!
165
166 ### Step 8: Create the DMG
167
168 ```bash
169 # Create temporary folder
170 mkdir dmg_temp
171 cp -r Sniffly.app dmg_temp/
172
173 # Add Applications folder shortcut
174 ln -s /Applications dmg_temp/Applications
175
176 # Create DMG
177 hdiutil create -volname "Sniffly" \
178 -srcfolder dmg_temp \
179 -ov -format UDZO \
180 Sniffly-0.2.0-macOS.dmg
181
182 # Cleanup
183 rm -rf dmg_temp
184 ```
185
186 ## Code Signing (Optional but Recommended)
187
188 If you have an Apple Developer account:
189
190 ```bash
191 # Sign the app
192 codesign --deep --force --verify --verbose \
193 --sign "Developer ID Application: Your Name (TEAM_ID)" \
194 Sniffly.app
195
196 # Verify signature
197 codesign -dv Sniffly.app
198
199 # Create DMG from signed app
200 # (repeat Step 8 after signing)
201
202 # Notarize (required for distribution outside App Store)
203 xcrun notarytool submit Sniffly-0.2.0-macOS.dmg \
204 --apple-id "your@email.com" \
205 --password "app-specific-password" \
206 --team-id "YOUR_TEAM_ID" \
207 --wait
208
209 # Staple notarization ticket
210 xcrun stapler staple Sniffly-0.2.0-macOS.dmg
211 ```
212
213 **Why sign and notarize?**
214 - Without: Users see "unidentified developer" warning
215 - With: Clean installation experience, no warnings
216
217 **Cost**: $99/year for Apple Developer account
218
219 ## Distribution
220
221 Once you have `Sniffly-0.2.0-macOS.dmg`:
222
223 1. **Test on a clean Mac** (or VM) without development tools installed
224 2. **Upload to your server**:
225 ```bash
226 scp Sniffly-0.2.0-macOS.dmg user@yourserver.com:/var/www/downloads/
227 ```
228 3. **Create a download page** with:
229 - Link to the DMG
230 - System requirements (macOS 11.0+)
231 - Installation instructions
232 - Screenshot/demo
233
234 ### Example Download Page
235
236 ```html
237 <!DOCTYPE html>
238 <html>
239 <head>
240 <title>Download Sniffly</title>
241 </head>
242 <body>
243 <h1>Download Sniffly v0.2.0</h1>
244 <p>A fast, visual disk space analyzer for macOS</p>
245
246 <a href="Sniffly-0.2.0-macOS.dmg" class="download-button">
247 Download for macOS (150 MB)
248 </a>
249
250 <h2>Requirements</h2>
251 <ul>
252 <li>macOS 11.0 (Big Sur) or later</li>
253 <li>Apple Silicon or Intel processor</li>
254 </ul>
255
256 <h2>Installation</h2>
257 <ol>
258 <li>Download Sniffly-0.2.0-macOS.dmg</li>
259 <li>Open the DMG file</li>
260 <li>Drag Sniffly to Applications folder</li>
261 <li>Launch from Applications</li>
262 </ol>
263 </body>
264 </html>
265 ```
266
267 ## Release Checklist
268
269 Before releasing a new version:
270
271 - [ ] Update `VERSION` file
272 - [ ] Test on clean macOS installation
273 - [ ] Verify `--version` shows correct version
274 - [ ] Test basic functionality (scan, navigation, etc.)
275 - [ ] Build release binary (`--buildtype=release`)
276 - [ ] Bundle GTK4 libraries with dylibbundler
277 - [ ] Test app bundle works standalone
278 - [ ] Create DMG
279 - [ ] (Optional) Code sign and notarize
280 - [ ] Upload to distribution server
281 - [ ] Update download page
282 - [ ] Announce release (social media, website, etc.)
283
284 ## Troubleshooting
285
286 ### "Sniffly is damaged and can't be opened"
287
288 This happens when macOS quarantines unsigned apps. Users can bypass with:
289 ```bash
290 xattr -cr /Applications/Sniffly.app
291 ```
292
293 Or you need to code sign and notarize the app.
294
295 ### App crashes immediately
296
297 - Check if GTK4 libraries are bundled: `ls Sniffly.app/Contents/Frameworks/`
298 - Should see many `.dylib` files
299 - Verify with: `otool -L Sniffly.app/Contents/MacOS/sniffly`
300 - Should show `@executable_path/../Frameworks/` paths
301
302 ### DMG too large (>200MB)
303
304 Normal! GTK4 is big. Options:
305 - Accept it (users download once)
306 - Require system GTK4 (smaller DMG, but users must `brew install gtk4`)
307
308 ### Can't sign the app
309
310 You need an Apple Developer account ($99/year). For free distribution:
311 - Skip signing (users will see warning)
312 - Document how to bypass Gatekeeper
313
314 ## Version Number Advice
315
316 You asked about version numbering. Here's my take:
317
318 **Current State of Sniffly (0.2.0 suggestion)**:
319 - ✅ Core functionality works (scanning, treemap, navigation)
320 - ✅ Progressive scanning is solid
321 - ✅ 3D effects, navigation locking
322 - ⚠️ Some rough edges (had bugs we fixed)
323 - ⚠️ No installers yet (this guide creates them)
324 - ❌ Not feature-complete vs original SpaceSniffer
325
326 **Version recommendations**:
327 - **0.1.0** - Tech preview, barely works
328 - **0.2.0** - Alpha release (current state) ← **I recommend this**
329 - **0.5.0** - Beta release (feature complete, but bugs)
330 - **0.9.0** - Release candidate
331 - **1.0.0** - Stable, production-ready
332
333 **Why 0.2.0?**
334 - Shows progress from initial experiments
335 - Sets expectations (alpha quality)
336 - Room to grow before 1.0
337 - Conservative (you prefer this)
338
339 **Semantic versioning for future**:
340 - `0.x.y` - Pre-1.0 development
341 - `1.0.0` - First stable release
342 - `1.1.0` - Add features (backwards compatible)
343 - `1.0.1` - Bug fixes
344 - `2.0.0` - Breaking changes
345
346 ## Next Steps
347
348 After successful macOS packaging, consider:
349 - Linux AppImage (simpler than .deb/.rpm)
350 - Homebrew cask formula (`brew install --cask sniffly`)
351 - Automatic updates (Sparkle framework)
352 - Analytics (crash reporting, usage stats)
353 - Website with documentation
354
355 ---
356
357 **Questions?** The packaging script at `scripts/package-macos.sh` automates all of this!