package main import ( "flag" "fmt" "image" "io" "os" "os/exec" "path/filepath" "strings" "text/template" "github.com/jackmordaunt/icns/v2" ) func main() { if err := run(); err != nil { fmt.Fprintf(os.Stderr, "%s", err) os.Exit(2) } } func run() error { var ( name = flag.String("name", "My Go Application", "app name") author = flag.String("author", "Appify by Machine Box", "author") version = flag.String("version", "1.0", "app version") identifier = flag.String("id", "", "bundle identifier") icon = flag.String("icon", "", "icon image file (.icns|.png|.jpg|.jpeg)") extraPlist = flag.String("plist", "", "path to file with additional plist dict key/value items") ) flag.Parse() args := flag.Args() if len(args) < 1 { return fmt.Errorf("missing executable argument") } bin := args[0] appname := *name + ".app" contentsPath := filepath.Join(appname, "Contents") appPath := filepath.Join(contentsPath, "MacOS") resouresPath := filepath.Join(contentsPath, "Resources") binPath := filepath.Join(appPath, appname) if err := os.MkdirAll(appPath, 0777); err != nil { return fmt.Errorf("os.MkdirAll appPath: %w", err) } fdst, err := os.Create(binPath) if err != nil { return fmt.Errorf("create bin: %w", err) } defer fdst.Close() fsrc, err := os.Open(bin) if err != nil { if os.IsNotExist(err) { return fmt.Errorf("%s not found", bin) } return fmt.Errorf("os.Open: %w", err) } defer fsrc.Close() if _, err := io.Copy(fdst, fsrc); err != nil { return fmt.Errorf("copy bin: %w", err) } if err := exec.Command("chmod", "+x", appPath).Run(); err != nil { return fmt.Errorf("chmod %s: %w", appPath, err) } if err := exec.Command("chmod", "+x", binPath).Run(); err != nil { return fmt.Errorf("chmod %s: %w", binPath, err) } id := *identifier if id == "" { id = *author + "." + *name } if *extraPlist != "" { b, err := os.ReadFile(*extraPlist) if err != nil { return fmt.Errorf("error reading plist location %s: %w", *extraPlist, err) } *extraPlist = string(b) } info := infoListData{ Name: *name, Executable: filepath.Join("MacOS", appname), Identifier: id, Version: *version, InfoString: *name + " by " + *author, ShortVersionString: *version, AdditionalPList: *extraPlist, } if *icon != "" { iconPath, err := prepareIcons(*icon, resouresPath) if err != nil { return fmt.Errorf("icon: %w", err) } info.IconFile = filepath.Base(iconPath) } tpl, err := template.New("template").Parse(infoPlistTemplate) if err != nil { return fmt.Errorf("infoPlistTemplate: %w", err) } fplist, err := os.Create(filepath.Join(contentsPath, "Info.plist")) if err != nil { return fmt.Errorf("create Info.plist: %w", err) } defer fplist.Close() if err := tpl.Execute(fplist, info); err != nil { return fmt.Errorf("execute Info.plist template: %w", err) } return nil } func prepareIcons(iconPath, resourcesPath string) (string, error) { ext := filepath.Ext(strings.ToLower(iconPath)) fsrc, err := os.Open(iconPath) if err != nil { if os.IsNotExist(err) { return "", fmt.Errorf("icon file not found") } return "", fmt.Errorf("open icon file: %w", err) } defer fsrc.Close() if err := os.MkdirAll(resourcesPath, 0777); err != nil { return "", fmt.Errorf("os.MkdirAll resourcesPath: %w", err) } destFile := filepath.Join(resourcesPath, "icon.icns") fdst, err := os.Create(destFile) if err != nil { return "", fmt.Errorf("create icon.icns file: %w", err) } defer fdst.Close() switch ext { case ".icns": // just copy the .icns file _, err := io.Copy(fdst, fsrc) if err != nil { return destFile, fmt.Errorf("copying %s: %w", iconPath, err) } case ".png", ".jpg", ".jpeg", ".gif": // process any images srcImg, _, err := image.Decode(fsrc) if err != nil { return destFile, fmt.Errorf("decode image: %w", err) } if err := icns.Encode(fdst, srcImg); err != nil { return destFile, fmt.Errorf("generate icns file: %w", err) } default: return destFile, fmt.Errorf("%s icons not supported", ext) } return destFile, nil } type infoListData struct { Name string Executable string Identifier string Version string InfoString string ShortVersionString string IconFile string AdditionalPList string } const infoPlistTemplate = ` CFBundlePackageType APPL CFBundleInfoDictionaryVersion 6.0 CFBundleName {{ .Name }} CFBundleExecutable {{ .Executable }} CFBundleIdentifier {{ .Identifier }} CFBundleVersion {{ .Version }} CFBundleGetInfoString {{ .InfoString }} CFBundleShortVersionString {{ .ShortVersionString }} {{ if .IconFile -}} CFBundleIconFile {{ .IconFile }} {{- end }} {{- if .AdditionalPList -}} {{ .AdditionalPList -}} {{- end }} `