Files
appify/main.go

191 lines
5.2 KiB
Go

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 = `<?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>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>{{ .Name }}</string>
<key>CFBundleExecutable</key>
<string>{{ .Executable }}</string>
<key>CFBundleIdentifier</key>
<string>{{ .Identifier }}</string>
<key>CFBundleVersion</key>
<string>{{ .Version }}</string>
<key>CFBundleGetInfoString</key>
<string>{{ .InfoString }}</string>
<key>CFBundleShortVersionString</key>
<string>{{ .ShortVersionString }}</string>
{{ if .IconFile -}}
<key>CFBundleIconFile</key>
<string>{{ .IconFile }}</string>
{{- end }}
{{- if .AdditionalPList -}}
{{ .AdditionalPList -}}
{{- end }}
</dict>
</plist>
`