added icon support
This commit is contained in:
80
main.go
80
main.go
@@ -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>
|
||||||
`
|
`
|
||||||
|
13
main_test.go
13
main_test.go
@@ -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
BIN
testdata/machina-square.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 136 KiB |
Reference in New Issue
Block a user