added icon support

This commit is contained in:
Mat Ryer
2018-05-08 13:31:24 +01:00
parent c0f645e3f7
commit ff64e7ccec
3 changed files with 78 additions and 15 deletions

80
main.go
View File

@@ -3,13 +3,16 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"image"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings"
"text/template" "text/template"
"github.com/JackMordaunt/icns"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@@ -26,6 +29,7 @@ func run() error {
author = flag.String("author", "Appify by Machine Box", "author") author = flag.String("author", "Appify by Machine Box", "author")
version = flag.String("version", "1.0", "app version") version = flag.String("version", "1.0", "app version")
identifier = flag.String("id", "", "bundle identifier") identifier = flag.String("id", "", "bundle identifier")
icon = flag.String("icon", "", "icon image file (.icns|.png|.jpg|.jpeg)")
) )
flag.Parse() flag.Parse()
args := flag.Args() args := flag.Args()
@@ -34,13 +38,14 @@ func run() error {
} }
bin := args[0] bin := args[0]
appname := *name + ".app" appname := *name + ".app"
contentspath := filepath.Join(appname, "Contents") contentsPath := filepath.Join(appname, "Contents")
apppath := filepath.Join(contentspath, "MacOS") appPath := filepath.Join(contentsPath, "MacOS")
binpath := filepath.Join(apppath, appname) resouresPath := filepath.Join(contentsPath, "Resources")
if err := os.MkdirAll(apppath, 0777); err != nil { binPath := filepath.Join(appPath, appname)
return errors.Wrap(err, "os.MkdirAll") 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 { if err != nil {
return errors.Wrap(err, "create bin") return errors.Wrap(err, "create bin")
} }
@@ -56,11 +61,11 @@ func run() error {
if _, err := io.Copy(fdst, fsrc); err != nil { if _, err := io.Copy(fdst, fsrc); err != nil {
return errors.Wrap(err, "copy bin") return errors.Wrap(err, "copy bin")
} }
if err := exec.Command("chmod", "+x", apppath).Run(); err != nil { if err := exec.Command("chmod", "+x", appPath).Run(); err != nil {
return errors.Wrap(err, "chmod: "+apppath) return errors.Wrap(err, "chmod: "+appPath)
} }
if err := exec.Command("chmod", "+x", binpath).Run(); err != nil { if err := exec.Command("chmod", "+x", binPath).Run(); err != nil {
return errors.Wrap(err, "chmod: "+binpath) return errors.Wrap(err, "chmod: "+binPath)
} }
id := *identifier id := *identifier
if id == "" { if id == "" {
@@ -74,11 +79,18 @@ func run() error {
InfoString: *name + " by " + *author, InfoString: *name + " by " + *author,
ShortVersionString: *version, 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) tpl, err := template.New("template").Parse(infoPlistTemplate)
if err != nil { if err != nil {
return errors.Wrap(err, "infoPlistTemplate") 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 { if err != nil {
return errors.Wrap(err, "create Info.plist") return errors.Wrap(err, "create Info.plist")
} }
@@ -86,12 +98,51 @@ func run() error {
if err := tpl.Execute(fplist, info); err != nil { if err := tpl.Execute(fplist, info); err != nil {
return errors.Wrap(err, "execute Info.plist template") 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 errors.Wrap(err, "ioutil.WriteFile")
} }
return nil 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 { type infoListData struct {
Name string Name string
Executable string Executable string
@@ -99,6 +150,7 @@ type infoListData struct {
Version string Version string
InfoString string InfoString string
ShortVersionString string ShortVersionString string
IconFile string
} }
const infoPlistTemplate = `<?xml version="1.0" encoding="UTF-8"?> const infoPlistTemplate = `<?xml version="1.0" encoding="UTF-8"?>
@@ -121,6 +173,10 @@ const infoPlistTemplate = `<?xml version="1.0" encoding="UTF-8"?>
<string>{{ .InfoString }}</string> <string>{{ .InfoString }}</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>{{ .ShortVersionString }}</string> <string>{{ .ShortVersionString }}</string>
{{ if .IconFile -}}
<key>CFBundleIconFile</key>
<string>{{ .IconFile }}</string>
{{- end }}
</dict> </dict>
</plist> </plist>
` `

View File

@@ -11,9 +11,14 @@ import (
"github.com/matryer/is" "github.com/matryer/is"
) )
// NOTE: Run these tests with `make test`
func Test(t *testing.T) { func Test(t *testing.T) {
is := is.New(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)) t.Logf("%q", string(out))
is.NoErr(err) is.NoErr(err)
defer os.RemoveAll("Test.app") 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", perm: "drwxr-xr-x"},
{path: "Test.app/Contents/MacOS", 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/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/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) { t.Run(f.path, func(t *testing.T) {
is := is.New(t) is := is.New(t)
info, err := os.Stat(f.path) info, err := os.Stat(f.path)
is.NoErr(err) is.NoErr(err)
is.Equal(info.Mode().String(), f.perm) is.Equal(info.Mode().String(), f.perm) // perm
if f.hash != "" { if f.hash != "" {
actual := filehash(t, f.path) actual := filehash(t, f.path)
is.Equal(actual, f.hash) // hash is.Equal(actual, f.hash) // hash

BIN
testdata/machina-square.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB