mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Add styled DMG with arrow pointing to Applications (#5445)
* Add styled DMG with arrow pointing to Applications - Add create-dmg as a bazel dependency for creating styled DMG installers - Add background image with arrow pointing from app to Applications - Update mac_build_handler to use create-dmg for styled DMG creation - Update CI workflow to use the new DMG creation process The DMG now shows a visual arrow guiding users to drag the app to the Applications folder. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add --skip-jenkins flag for headless CI environment The AppleScript that positions icons times out in CI environments without a GUI session. Using --skip-jenkins skips the Finder styling while still including the background image and Applications link. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Switch from create-dmg to dmgbuild for CI compatibility dmgbuild generates .DS_Store files programmatically without needing AppleScript or Finder access, making it work in headless CI environments. - Remove create-dmg bazel dependency (relied on AppleScript) - Use dmgbuild Python tool instead (pip install dmgbuild) - Generates proper icon positions and background without GUI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix DMG styling: lighter background, adjust icon positions - Use light gray background so file/folder names are readable - Move arrow up to align with icon centers - Move Applications link further right (500 -> 520) to clear the arrow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix arrow: move left, connect head to shaft properly - Arrow now starts at x=200 (closer to app icon) - Arrow ends at x=460 (further from Applications at x=520) - Head and shaft now overlap for proper connection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -263,6 +263,12 @@ jobs:
|
||||
echo "$SPARKLE_EDDSA_PRIVATE_KEY" > "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
chmod 600 "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
# Install dmgbuild (creates .DS_Store programmatically, no AppleScript needed)
|
||||
pip3 install dmgbuild
|
||||
|
||||
# Background image for styled DMG
|
||||
BACKGROUND_PATH="$(pwd)/ci/mac/dmg/background.png"
|
||||
|
||||
# Use commit count for automatic incrementing versions (e.g., 1.0.9548)
|
||||
BUILD_NUMBER=$(git rev-list --count HEAD)
|
||||
VERSION="1.0.${BUILD_NUMBER}"
|
||||
@@ -271,6 +277,7 @@ jobs:
|
||||
"/tmp/eagle0/eagle0MAC/eagle0.app" \
|
||||
"$VERSION" \
|
||||
"$BUILD_NUMBER" \
|
||||
"$BACKGROUND_PATH" \
|
||||
"$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
rm "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
BIN
Binary file not shown.
@@ -51,47 +51,72 @@ type Enclosure struct {
|
||||
EdSig string `xml:"sparkle:edSignature,attr"`
|
||||
}
|
||||
|
||||
func createDMG(appPath string, dmgPath string, volumeName string) error {
|
||||
// Create a temporary directory for the DMG contents
|
||||
tmpDir, err := os.MkdirTemp("", "dmg-contents-")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Copy the app to the temp directory
|
||||
appName := filepath.Base(appPath)
|
||||
destApp := filepath.Join(tmpDir, appName)
|
||||
log.Printf("Copying %s to %s", appPath, destApp)
|
||||
|
||||
// Use ditto to preserve all metadata, permissions, and symlinks
|
||||
cmd := exec.Command("ditto", appPath, destApp)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to copy app: %s: %w", string(output), err)
|
||||
}
|
||||
|
||||
// Create an alias to /Applications
|
||||
applicationsAlias := filepath.Join(tmpDir, "Applications")
|
||||
cmd = exec.Command("ln", "-s", "/Applications", applicationsAlias)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create Applications symlink: %s: %w", string(output), err)
|
||||
}
|
||||
|
||||
func createDMG(appPath string, dmgPath string, volumeName string, backgroundPath string) error {
|
||||
// Remove any existing DMG
|
||||
os.Remove(dmgPath)
|
||||
|
||||
// Create the DMG using hdiutil
|
||||
// -volname: Name shown when mounted
|
||||
// -srcfolder: Source directory
|
||||
// -ov: Overwrite existing
|
||||
// -format UDZO: Compressed DMG (read-only)
|
||||
log.Printf("Creating DMG: %s", dmgPath)
|
||||
cmd = exec.Command("hdiutil", "create",
|
||||
"-volname", volumeName,
|
||||
"-srcfolder", tmpDir,
|
||||
"-ov",
|
||||
"-format", "UDZO",
|
||||
dmgPath)
|
||||
log.Printf("Creating styled DMG: %s", dmgPath)
|
||||
|
||||
// Create a temporary dmgbuild settings file
|
||||
settingsFile, err := os.CreateTemp("", "dmgbuild-settings-*.py")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create settings file: %w", err)
|
||||
}
|
||||
defer os.Remove(settingsFile.Name())
|
||||
|
||||
appName := filepath.Base(appPath)
|
||||
|
||||
// dmgbuild settings - generates .DS_Store programmatically without AppleScript
|
||||
settings := fmt.Sprintf(`
|
||||
# dmgbuild settings for Eagle0
|
||||
import os
|
||||
|
||||
# Volume name
|
||||
volume_name = %q
|
||||
|
||||
# Output format
|
||||
format = 'UDZO'
|
||||
|
||||
# Volume size (None = auto)
|
||||
size = None
|
||||
|
||||
# Files to include
|
||||
files = [
|
||||
(%q, %q),
|
||||
]
|
||||
|
||||
# Symlinks
|
||||
symlinks = {
|
||||
'Applications': '/Applications',
|
||||
}
|
||||
|
||||
# Background image
|
||||
background = %q
|
||||
|
||||
# Window settings
|
||||
icon_size = 128
|
||||
icon_locations = {
|
||||
%q: (140, 180),
|
||||
'Applications': (520, 180),
|
||||
}
|
||||
|
||||
# Window position and size
|
||||
window_rect = ((100, 100), (660, 400))
|
||||
|
||||
# Hide standard folders
|
||||
hide_extensions = ['app']
|
||||
`, volumeName, appPath, appName, backgroundPath, appName)
|
||||
|
||||
if _, err := settingsFile.WriteString(settings); err != nil {
|
||||
return fmt.Errorf("failed to write settings: %w", err)
|
||||
}
|
||||
settingsFile.Close()
|
||||
|
||||
// Run dmgbuild
|
||||
cmd := exec.Command("dmgbuild", "-s", settingsFile.Name(), volumeName, dmgPath)
|
||||
cmd.Env = append(os.Environ(), "PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:"+os.Getenv("HOME")+"/.local/bin")
|
||||
|
||||
log.Printf("Running: dmgbuild -s %s %s %s", settingsFile.Name(), volumeName, dmgPath)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create DMG: %s: %w", string(output), err)
|
||||
}
|
||||
@@ -198,34 +223,39 @@ func uploadAppcast(bb aws.BucketBasics, appcast *Appcast) error {
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Println("Usage: mac_build_handler <app_path> <version> <build_number> [sparkle_private_key_path]")
|
||||
if len(os.Args) < 5 {
|
||||
fmt.Println("Usage: mac_build_handler <app_path> <version> <build_number> <background_path> [sparkle_private_key_path]")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
appPath := os.Args[1]
|
||||
version := os.Args[2]
|
||||
buildNumber := os.Args[3]
|
||||
backgroundPath := os.Args[4]
|
||||
privateKeyPath := ""
|
||||
if len(os.Args) >= 5 {
|
||||
privateKeyPath = os.Args[4]
|
||||
if len(os.Args) >= 6 {
|
||||
privateKeyPath = os.Args[5]
|
||||
}
|
||||
|
||||
if _, err := os.Stat(appPath); os.IsNotExist(err) {
|
||||
log.Fatalf("App not found: %s", appPath)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(backgroundPath); os.IsNotExist(err) {
|
||||
log.Fatalf("Background image not found: %s", backgroundPath)
|
||||
}
|
||||
|
||||
bb, err := aws.NewBucketBasics()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Create DMG of the app
|
||||
// Create DMG of the app using dmgbuild (generates .DS_Store programmatically)
|
||||
dmgFileName := fmt.Sprintf("eagle0-%s.dmg", version)
|
||||
dmgPath := filepath.Join("/tmp", dmgFileName)
|
||||
|
||||
log.Printf("Creating DMG: %s", dmgPath)
|
||||
if err := createDMG(appPath, dmgPath, "Eagle0"); err != nil {
|
||||
if err := createDMG(appPath, dmgPath, "Eagle0", backgroundPath); err != nil {
|
||||
log.Fatalf("Failed to create DMG: %v", err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user