initial commit
This commit is contained in:
6
Makefile
Normal file
6
Makefile
Normal file
@@ -0,0 +1,6 @@
|
||||
test:
|
||||
go build -o testdata/app testdata/app.go
|
||||
go build -o appify
|
||||
go test
|
||||
rm appify
|
||||
rm testdata/app
|
15
README.md
15
README.md
@@ -1,2 +1,15 @@
|
||||
# appify
|
||||
Create a macOS Application from a Go binary
|
||||
|
||||
Create a macOS Application from an executable (like a Go binary)
|
||||
|
||||
## Install
|
||||
|
||||
To install `appify`:
|
||||
|
||||
```bash
|
||||
go get github.com/machinebox/appify
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
appify -name "My Go Application" /path/to/bin
|
||||
|
134
main.go
Normal file
134
main.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
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")
|
||||
)
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
if len(args) < 1 {
|
||||
return errors.New("missing executable argument")
|
||||
}
|
||||
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")
|
||||
}
|
||||
fdst, err := os.Create(binpath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create bin")
|
||||
}
|
||||
defer fdst.Close()
|
||||
fsrc, err := os.Open(bin)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errors.New(bin + " not found")
|
||||
}
|
||||
return errors.Wrap(err, "os.Open")
|
||||
}
|
||||
defer fsrc.Close()
|
||||
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", binpath).Run(); err != nil {
|
||||
return errors.Wrap(err, "chmod: "+binpath)
|
||||
}
|
||||
id := *identifier
|
||||
if id == "" {
|
||||
id = *author + "." + *name
|
||||
}
|
||||
info := infoListData{
|
||||
Name: *name,
|
||||
Executable: filepath.Join("MacOS", appname),
|
||||
Identifier: id,
|
||||
Version: *version,
|
||||
InfoString: *name + " by " + *author,
|
||||
ShortVersionString: *version,
|
||||
}
|
||||
tpl, err := template.New("template").Parse(infoPlistTemplate)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "infoPlistTemplate")
|
||||
}
|
||||
fplist, err := os.Create(filepath.Join(contentspath, "Info.plist"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create Info.plist")
|
||||
}
|
||||
defer fplist.Close()
|
||||
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 {
|
||||
return errors.Wrap(err, "ioutil.WriteFile")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type infoListData struct {
|
||||
Name string
|
||||
Executable string
|
||||
Identifier string
|
||||
Version string
|
||||
InfoString string
|
||||
ShortVersionString 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>
|
||||
</dict>
|
||||
</plist>
|
||||
`
|
||||
|
||||
// readme goes into a README file inside the package for
|
||||
// future reference.
|
||||
const readme = `Made with Appify by Machine Box
|
||||
https://github.com/machinebox/appify
|
||||
|
||||
Inspired by https://gist.github.com/anmoljagetia/d37da67b9d408b35ac753ce51e420132
|
||||
`
|
57
main_test.go
Normal file
57
main_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
is := is.New(t)
|
||||
out, err := exec.Command("./appify", "-name", "Test", "testdata/app").CombinedOutput()
|
||||
t.Logf("%q", string(out))
|
||||
is.NoErr(err)
|
||||
defer os.RemoveAll("Test.app")
|
||||
actualAppHash := filehash(t, "testdata/app")
|
||||
type file struct {
|
||||
path string
|
||||
perm string
|
||||
hash string
|
||||
}
|
||||
for _, f := range []file{
|
||||
{path: "Test.app", 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/Test.app", perm: "-rwxr-xr-x", hash: actualAppHash},
|
||||
{path: "Test.app/Contents/Info.plist", perm: "-rw-r--r--", hash: "d263b0111cec1e6677970a35cc52f14d"},
|
||||
{path: "Test.app/Contents/README", perm: "-rw-r--r--", hash: "afeb10df47c7f189b848ae44a54e7e06"},
|
||||
} {
|
||||
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)
|
||||
if f.hash != "" {
|
||||
actual := filehash(t, f.path)
|
||||
is.Equal(actual, f.hash) // hash
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// filehash gets an md5 hash of the file at path.
|
||||
func filehash(t *testing.T, path string) string {
|
||||
is := is.New(t)
|
||||
f, err := os.Open(path)
|
||||
is.NoErr(err)
|
||||
defer f.Close()
|
||||
h := md5.New()
|
||||
_, err = io.Copy(h, f)
|
||||
is.NoErr(err)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
7
testdata/app.go
vendored
Normal file
7
testdata/app.go
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "log"
|
||||
|
||||
func main() {
|
||||
log.Println("Hello world of desktop applications")
|
||||
}
|
Reference in New Issue
Block a user