diff --git a/main.go b/main.go index 48522b3..ed63160 100644 --- a/main.go +++ b/main.go @@ -3,13 +3,16 @@ package main import ( "flag" "fmt" + "image" "io" "io/ioutil" "os" "os/exec" "path/filepath" + "strings" "text/template" + "github.com/JackMordaunt/icns" "github.com/pkg/errors" ) @@ -26,6 +29,7 @@ func run() error { 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)") ) flag.Parse() args := flag.Args() @@ -34,13 +38,14 @@ func run() error { } bin := args[0] appname := *name + ".app" - contentspath := filepath.Join(appname, "Contents") - apppath := filepath.Join(contentspath, "MacOS") - binpath := filepath.Join(apppath, appname) - if err := os.MkdirAll(apppath, 0777); err != nil { - return errors.Wrap(err, "os.MkdirAll") + 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 errors.Wrap(err, "os.MkdirAll appPath") } - fdst, err := os.Create(binpath) + fdst, err := os.Create(binPath) if err != nil { return errors.Wrap(err, "create bin") } @@ -56,11 +61,11 @@ func run() error { if _, err := io.Copy(fdst, fsrc); err != nil { return errors.Wrap(err, "copy bin") } - if err := exec.Command("chmod", "+x", apppath).Run(); err != nil { - return errors.Wrap(err, "chmod: "+apppath) + if err := exec.Command("chmod", "+x", appPath).Run(); err != nil { + return errors.Wrap(err, "chmod: "+appPath) } - if err := exec.Command("chmod", "+x", binpath).Run(); err != nil { - return errors.Wrap(err, "chmod: "+binpath) + if err := exec.Command("chmod", "+x", binPath).Run(); err != nil { + return errors.Wrap(err, "chmod: "+binPath) } id := *identifier if id == "" { @@ -74,11 +79,18 @@ func run() error { InfoString: *name + " by " + *author, ShortVersionString: *version, } + if *icon != "" { + iconPath, err := prepareIcons(*icon, resouresPath) + if err != nil { + return errors.Wrap(err, "icon") + } + info.IconFile = filepath.Base(iconPath) + } tpl, err := template.New("template").Parse(infoPlistTemplate) if err != nil { return errors.Wrap(err, "infoPlistTemplate") } - fplist, err := os.Create(filepath.Join(contentspath, "Info.plist")) + fplist, err := os.Create(filepath.Join(contentsPath, "Info.plist")) if err != nil { return errors.Wrap(err, "create Info.plist") } @@ -86,12 +98,51 @@ func run() error { if err := tpl.Execute(fplist, info); err != nil { return errors.Wrap(err, "execute Info.plist template") } - if err := ioutil.WriteFile(filepath.Join(contentspath, "README"), []byte(readme), 0666); err != nil { + if err := ioutil.WriteFile(filepath.Join(contentsPath, "README"), []byte(readme), 0666); err != nil { return errors.Wrap(err, "ioutil.WriteFile") } 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 "", errors.New("icon file not found") + } + return "", errors.Wrap(err, "open icon file") + } + defer fsrc.Close() + if err := os.MkdirAll(resourcesPath, 0777); err != nil { + return "", errors.Wrap(err, "os.MkdirAll resourcesPath") + } + destFile := filepath.Join(resourcesPath, "icon.icns") + fdst, err := os.Create(destFile) + if err != nil { + return "", errors.Wrap(err, "create icon.icns file") + } + defer fdst.Close() + switch ext { + case ".icns": // just copy the .icns file + _, err := io.Copy(fdst, fsrc) + if err != nil { + return destFile, errors.Wrap(err, "copying "+iconPath) + } + case ".png", ".jpg", ".jpeg", ".gif": // process any images + srcImg, _, err := image.Decode(fsrc) + if err != nil { + return destFile, errors.Wrap(err, "decode image") + } + if err := icns.Encode(fdst, srcImg); err != nil { + return destFile, errors.Wrap(err, "generate icns file") + } + default: + return destFile, errors.New(ext + " icons not supported") + } + return destFile, nil +} + type infoListData struct { Name string Executable string @@ -99,6 +150,7 @@ type infoListData struct { Version string InfoString string ShortVersionString string + IconFile string } const infoPlistTemplate = ` @@ -121,6 +173,10 @@ const infoPlistTemplate = ` {{ .InfoString }} CFBundleShortVersionString {{ .ShortVersionString }} + {{ if .IconFile -}} + CFBundleIconFile + {{ .IconFile }} + {{- end }} ` diff --git a/main_test.go b/main_test.go index aceb348..656eb23 100644 --- a/main_test.go +++ b/main_test.go @@ -11,9 +11,14 @@ import ( "github.com/matryer/is" ) +// NOTE: Run these tests with `make test` + func Test(t *testing.T) { is := is.New(t) - out, err := exec.Command("./appify", "-name", "Test", "testdata/app").CombinedOutput() + out, err := exec.Command("./appify", + "-name", "Test", + "-icon", "testdata/machina-square.png", + "testdata/app").CombinedOutput() t.Logf("%q", string(out)) is.NoErr(err) defer os.RemoveAll("Test.app") @@ -28,14 +33,16 @@ func Test(t *testing.T) { {path: "Test.app/Contents", perm: "drwxr-xr-x"}, {path: "Test.app/Contents/MacOS", perm: "drwxr-xr-x"}, {path: "Test.app/Contents/MacOS/Test.app", perm: "-rwxr-xr-x", hash: actualAppHash}, - {path: "Test.app/Contents/Info.plist", perm: "-rw-r--r--", hash: "d263b0111cec1e6677970a35cc52f14d"}, + {path: "Test.app/Contents/Info.plist", perm: "-rw-r--r--", hash: "0cd092b7b884e87617648dbdadb6a804"}, {path: "Test.app/Contents/README", perm: "-rw-r--r--", hash: "afeb10df47c7f189b848ae44a54e7e06"}, + {path: "Test.app/Contents/Resources", perm: "drwxr-xr-x"}, + {path: "Test.app/Contents/Resources/icon.icns", perm: "-rw-r--r--", hash: "23bdc36475094ed8886f319811d3a182"}, } { t.Run(f.path, func(t *testing.T) { is := is.New(t) info, err := os.Stat(f.path) is.NoErr(err) - is.Equal(info.Mode().String(), f.perm) + is.Equal(info.Mode().String(), f.perm) // perm if f.hash != "" { actual := filehash(t, f.path) is.Equal(actual, f.hash) // hash diff --git a/testdata/machina-square.png b/testdata/machina-square.png new file mode 100644 index 0000000..958836b Binary files /dev/null and b/testdata/machina-square.png differ