From 3e9f7fce4e2ff6be2524b65859e0d9846cd3f2b4 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sun, 15 Jun 2025 23:55:39 +1000 Subject: [PATCH] Fix DMG import --- v3/internal/commands/dmg/dmg.go | 157 ++++++++++++++++++++++++++++++++ v3/internal/commands/msix.go | 61 ++++++------- 2 files changed, 186 insertions(+), 32 deletions(-) create mode 100644 v3/internal/commands/dmg/dmg.go diff --git a/v3/internal/commands/dmg/dmg.go b/v3/internal/commands/dmg/dmg.go new file mode 100644 index 000000000..7ba2de572 --- /dev/null +++ b/v3/internal/commands/dmg/dmg.go @@ -0,0 +1,157 @@ +package dmg + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +// Creator handles DMG creation +type Creator struct { + sourcePath string + outputPath string + appName string + backgroundImage string + iconPositions map[string]Position +} + +// Position represents icon coordinates in the DMG +type Position struct { + X, Y int +} + +// New creates a new DMG creator +func New(sourcePath, outputPath, appName string) (*Creator, error) { + if runtime.GOOS != "darwin" { + return nil, fmt.Errorf("DMG creation is only supported on macOS") + } + + // Check if source exists + if _, err := os.Stat(sourcePath); os.IsNotExist(err) { + return nil, fmt.Errorf("source path does not exist: %s", sourcePath) + } + + return &Creator{ + sourcePath: sourcePath, + outputPath: outputPath, + appName: appName, + iconPositions: make(map[string]Position), + }, nil +} + +// SetBackgroundImage sets the background image for the DMG +func (c *Creator) SetBackgroundImage(imagePath string) error { + if _, err := os.Stat(imagePath); os.IsNotExist(err) { + return fmt.Errorf("background image does not exist: %s", imagePath) + } + c.backgroundImage = imagePath + return nil +} + +// AddIconPosition adds an icon position for the DMG layout +func (c *Creator) AddIconPosition(filename string, x, y int) { + c.iconPositions[filename] = Position{X: x, Y: y} +} + +// Create creates the DMG file +func (c *Creator) Create() error { + // Remove existing DMG if it exists + if _, err := os.Stat(c.outputPath); err == nil { + if err := os.Remove(c.outputPath); err != nil { + return fmt.Errorf("failed to remove existing DMG: %w", err) + } + } + + // Create a temporary directory for DMG content + tempDir, err := os.MkdirTemp("", "dmg-*") + if err != nil { + return fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tempDir) + + // Copy the app bundle to temp directory + appName := filepath.Base(c.sourcePath) + tempAppPath := filepath.Join(tempDir, appName) + if err := c.copyDir(c.sourcePath, tempAppPath); err != nil { + return fmt.Errorf("failed to copy app bundle: %w", err) + } + + // Create Applications symlink + applicationsLink := filepath.Join(tempDir, "Applications") + if err := os.Symlink("/Applications", applicationsLink); err != nil { + return fmt.Errorf("failed to create Applications symlink: %w", err) + } + + // Copy background image if provided + if c.backgroundImage != "" { + bgName := filepath.Base(c.backgroundImage) + bgPath := filepath.Join(tempDir, bgName) + if err := c.copyFile(c.backgroundImage, bgPath); err != nil { + return fmt.Errorf("failed to copy background image: %w", err) + } + } + + // Create DMG using hdiutil + if err := c.createDMGWithHdiutil(tempDir); err != nil { + return fmt.Errorf("failed to create DMG with hdiutil: %w", err) + } + + return nil +} + +// copyDir recursively copies a directory +func (c *Creator) copyDir(src, dst string) error { + cmd := exec.Command("cp", "-R", src, dst) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to copy directory: %w", err) + } + return nil +} + +// copyFile copies a file +func (c *Creator) copyFile(src, dst string) error { + cmd := exec.Command("cp", src, dst) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to copy file: %w", err) + } + return nil +} + +// createDMGWithHdiutil creates the DMG using macOS hdiutil +func (c *Creator) createDMGWithHdiutil(sourceDir string) error { + // Calculate size needed for DMG (roughly 2x the source size for safety) + sizeCmd := exec.Command("du", "-sk", sourceDir) + output, err := sizeCmd.Output() + if err != nil { + return fmt.Errorf("failed to calculate directory size: %w", err) + } + + // Parse size and add padding + sizeStr := strings.Fields(string(output))[0] + + // Create DMG with hdiutil + args := []string{ + "create", + "-srcfolder", sourceDir, + "-format", "UDZO", + "-volname", c.appName, + c.outputPath, + } + + // Add size if we could determine it + if sizeStr != "" { + // Add 50% padding to the calculated size + args = append([]string{"create", "-size", sizeStr + "k"}, args[1:]...) + args[0] = "create" + } + + cmd := exec.Command("hdiutil", args...) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("hdiutil failed: %w\nOutput: %s", err, string(output)) + } + + return nil +} diff --git a/v3/internal/commands/msix.go b/v3/internal/commands/msix.go index 457165fc7..6c803e6d8 100644 --- a/v3/internal/commands/msix.go +++ b/v3/internal/commands/msix.go @@ -8,12 +8,9 @@ import ( "os/exec" "path/filepath" "runtime" - "strings" "text/template" - "github.com/leaanthony/gosod" "github.com/wailsapp/wails/v3/internal/flags" - "github.com/wailsapp/wails/v3/internal/s" ) //go:embed build_assets/windows/msix/* @@ -23,13 +20,13 @@ var msixAssets embed.FS type MSIXOptions struct { // Info from project config Info struct { - CompanyName string `json:"companyName"` - ProductName string `json:"productName"` - ProductVersion string `json:"version"` + CompanyName string `json:"companyName"` + ProductName string `json:"productName"` + ProductVersion string `json:"version"` ProductIdentifier string `json:"productIdentifier"` - Description string `json:"description"` - Copyright string `json:"copyright"` - Comments string `json:"comments"` + Description string `json:"description"` + Copyright string `json:"copyright"` + Comments string `json:"comments"` } // File associations FileAssociations []struct { @@ -41,15 +38,15 @@ type MSIXOptions struct { MimeType string `json:"mimeType,omitempty"` } `json:"fileAssociations"` // MSIX specific options - Publisher string `json:"publisher"` - CertificatePath string `json:"certificatePath"` - CertificatePassword string `json:"certificatePassword,omitempty"` + Publisher string `json:"publisher"` + CertificatePath string `json:"certificatePath"` + CertificatePassword string `json:"certificatePassword,omitempty"` ProcessorArchitecture string `json:"processorArchitecture"` - ExecutableName string `json:"executableName"` - ExecutablePath string `json:"executablePath"` - OutputPath string `json:"outputPath"` - UseMsixPackagingTool bool `json:"useMsixPackagingTool"` - UseMakeAppx bool `json:"useMakeAppx"` + ExecutableName string `json:"executableName"` + ExecutablePath string `json:"executablePath"` + OutputPath string `json:"outputPath"` + UseMsixPackagingTool bool `json:"useMsixPackagingTool"` + UseMakeAppx bool `json:"useMakeAppx"` } // ToolMSIX creates an MSIX package for Windows applications @@ -79,7 +76,7 @@ func ToolMSIX(options *flags.ToolMSIX) error { // Parse the config var config struct { - Info map[string]interface{} `json:"info"` + Info map[string]interface{} `json:"info"` FileAssociations []map[string]interface{} `json:"fileAssociations"` } if err := json.Unmarshal(configData, &config); err != nil { @@ -88,15 +85,15 @@ func ToolMSIX(options *flags.ToolMSIX) error { // Create MSIX options msixOptions := MSIXOptions{ - Publisher: options.Publisher, - CertificatePath: options.CertificatePath, - CertificatePassword: options.CertificatePassword, + Publisher: options.Publisher, + CertificatePath: options.CertificatePath, + CertificatePassword: options.CertificatePassword, ProcessorArchitecture: options.Arch, - ExecutableName: options.ExecutableName, - ExecutablePath: options.ExecutablePath, - OutputPath: options.OutputPath, - UseMsixPackagingTool: options.UseMsixPackagingTool, - UseMakeAppx: options.UseMakeAppx, + ExecutableName: options.ExecutableName, + ExecutablePath: options.ExecutablePath, + OutputPath: options.OutputPath, + UseMsixPackagingTool: options.UseMsixPackagingTool, + UseMakeAppx: options.UseMakeAppx, } // Copy info from config @@ -239,7 +236,7 @@ func createMSIXWithPackagingTool(options *MSIXOptions) error { // Create the MSIX package fmt.Println("Creating MSIX package using Microsoft MSIX Packaging Tool...") args := []string{"create-package", "--template", templatePath} - + // Add certificate password if provided if options.CertificatePassword != "" { args = append(args, "--certPassword", options.CertificatePassword) @@ -283,14 +280,14 @@ func createMSIXWithMakeAppx(options *MSIXOptions) error { if options.CertificatePath != "" { fmt.Println("Signing MSIX package...") signArgs := []string{"sign", "/fd", "SHA256", "/a", "/f", options.CertificatePath} - + // Add certificate password if provided if options.CertificatePassword != "" { signArgs = append(signArgs, "/p", options.CertificatePassword) } - + signArgs = append(signArgs, options.OutputPath) - + cmd = exec.Command("signtool.exe", signArgs...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -414,7 +411,7 @@ func generateAppxManifest(options *MSIXOptions, outputPath string) error { func generatePlaceholderImage(outputPath string) error { // For now, we'll create a simple 1x1 transparent PNG // In a real implementation, we would generate proper icons based on the application icon - + // Create a minimal valid PNG file (1x1 transparent pixel) pngData := []byte{ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, @@ -469,7 +466,7 @@ func InstallMSIXTools() error { if !sdkInstalled { fmt.Println("Windows SDK is not installed. Please download and install from:") fmt.Println("https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/") - + // Open the download page cmd = exec.Command("powershell", "-Command", "Start-Process https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/") if err := cmd.Run(); err != nil {