commit e9fa62acad52bf535b385ea178f4f557b88a182b
parent 9a6a96f67d900d317e8728855ac261bd10813e74
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date: Mon, 19 Nov 2018 15:33:13 +1300
[+] Build script for creating distribution ready packages.
Only MacOS supported initially.
Diffstat:
4 files changed, 494 insertions(+), 0 deletions(-)
diff --git a/cmd/build/assets/giffer.icns b/cmd/build/assets/giffer.icns
Binary files differ.
diff --git a/cmd/build/main.go b/cmd/build/main.go
@@ -0,0 +1,313 @@
+// build functions as a build script, building and bundling the package.
+package main
+
+import (
+ "archive/zip"
+ "bytes"
+ "flag"
+ "fmt"
+ "image/png"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ "github.com/jackmordaunt/icns"
+ "github.com/pkg/errors"
+)
+
+const plist = `
+<?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>CFBundleExecutable</key>
+ <string>giffer</string>
+ <key>CFBundleIconFile</key>
+ <string>icon.icns</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.jackmordaunt.giffer</string>
+ <key>NSHighResolutionCapable</key>
+ <true/>
+ <key>NSSupportsAutomaticGraphicsSwitching</key>
+ <true/>
+ <key>NSAppTransportSecurity</key>
+ <dict>
+ <key>NSExceptionDomains</key>
+ <dict>
+ <key>localhost</key>
+ <dict>
+ <key>NSExceptionAllowsInsecureHTTPLoads</key>
+ <true/>
+ <key>NSIncludesSubdomains</key>
+ <true/>
+ </dict>
+ </dict>
+ </dict>
+</dict>
+</plist>
+`
+
+// <key>ProgramArguments</key>
+// <array>
+// <string>giffer</string>
+// <string>-v</string>
+// <string>-ffmpeg</string>
+// <string>Resources/ffmpeg</string>
+// </array>
+
+var (
+ dist string
+ tags string
+ icon string
+ ffmpeg string
+)
+
+func init() {
+ flag.StringVar(&dist, "dist", "dist", "folder to put artifact")
+ flag.StringVar(&icon, "icon", "", "path to icon file (.png or .icns)")
+ flag.StringVar(&tags, "tags", "", "build tags to pass to go build")
+ flag.StringVar(&ffmpeg, "ffmpeg", "", "path to ffmpeg binary")
+ flag.Parse()
+}
+
+func main() {
+ if runtime.GOOS == "darwin" {
+ var (
+ contents = filepath.Join(dist, "Giffer.app", "Contents")
+ macos = filepath.Join(dist, "Giffer.app", "Contents", "MacOS")
+ resources = filepath.Join(dist, "Giffer.app", "Contents", "Resources")
+ info = filepath.Join(dist, "Giffer.app", "Contents", "Info.plist")
+ iconf = filepath.Join(dist, "Giffer.app", "Contents", "Resources", "icon.icns")
+ frameworks = filepath.Join(dist, "Giffer.app", "Contents", "Frameworks")
+ )
+ for _, dir := range []string{contents, macos, resources, frameworks} {
+ if err := os.MkdirAll(dir, 0755); err != nil && !os.IsExist(err) {
+ log.Fatalf("preparing directory: %v", err)
+ }
+ }
+ tasks := FanOut([]Task{
+ Task{
+ Name: "compile ui",
+ Op: func() error {
+ cmd := exec.Command("yarn", "build")
+ cmd.Dir = "/Users/jack/dev/personal/giffer/cmd/desktop/ui"
+ if out, err := cmd.CombinedOutput(); err != nil {
+ return errors.Wrap(err, string(out))
+ }
+ return nil
+ },
+ },
+ Task{
+ Requires: []string{"compile ui"},
+ Name: "compile binary",
+ Op: func() error {
+ args := []string{"go", "build"}
+ if tags != "" {
+ args = append(args, "-tags", tags)
+ }
+ args = append(args, []string{
+ "-o", filepath.Join(macos, "giffer"),
+ "github.com/jackmordaunt/giffer/cmd/desktop",
+ }...)
+ if out, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil {
+ return fmt.Errorf("%s: %v", string(out), err)
+ }
+ return nil
+ },
+ },
+ Task{
+ Requires: []string{"compile binary"},
+ Name: "embed resources",
+ Op: func() error {
+ cmd := exec.Command(
+ "rice",
+ "append",
+ "--exec", filepath.Join(macos, "giffer"),
+ )
+ cmd.Dir = "/Users/jack/dev/personal/giffer/cmd/build"
+ if out, err := cmd.CombinedOutput(); err != nil {
+ return errors.Wrap(err, string(out))
+ }
+ return nil
+ },
+ },
+ Task{
+ Name: "create icon.icns",
+ Op: func() error {
+ if icon != "" {
+ if err := WriteIcon(icon, iconf); err != nil {
+ return errors.Wrap(err, "writing icon")
+ }
+ }
+ return nil
+ },
+ },
+ Task{
+ Name: "create Info.plist",
+ Op: func() error {
+ if err := ioutil.WriteFile(info, []byte(plist), 0644); err != nil {
+ return errors.Wrap(err, "writing plist")
+ }
+ return nil
+ },
+ },
+ Task{
+ Name: "bundle ffmpeg",
+ Op: func() error {
+ var (
+ src io.Reader
+ err error
+ )
+ if ffmpeg == "" {
+ ffmpeg = "https://ffmpeg.zeranoe.com/builds/macos64/static/ffmpeg-latest-macos64-static.zip"
+ }
+ if strings.Contains(ffmpeg, "https://") {
+ u, err := user.Current()
+ if err != nil {
+ return errors.Wrap(err, "determine current user")
+ }
+ downloads := filepath.Join(u.HomeDir, "Downloads")
+ src, err = openZip(ffmpeg, downloads, "bin/ffmpeg")
+ if err != nil {
+ return errors.Wrap(err, "downloading ffmpeg")
+ }
+ } else {
+ srcf, err := os.Open(ffmpeg)
+ if err != nil {
+ return errors.Wrap(err, "opening ffmpeg file")
+ }
+ defer srcf.Close()
+ src = srcf
+ }
+ destf, err := os.OpenFile(
+ filepath.Join(resources, "ffmpeg"),
+ os.O_CREATE|os.O_RDWR,
+ 0777)
+ if err != nil {
+ return errors.Wrap(err, "opening desintaiton file")
+ }
+ defer destf.Close()
+ if _, err := io.Copy(destf, src); err != nil {
+ return errors.Wrap(err, "copying")
+ }
+ return nil
+ },
+ },
+ })
+ if err := tasks.Run(); err != nil {
+ log.Fatalf("packaging giffer: %v", err)
+ }
+ }
+}
+
+// WriteIcon writes out the icon.icns using specified source icon.
+// If the source icon is a .png then it is converted to .icns.
+func WriteIcon(in, out string) error {
+ var (
+ src io.ReadWriter
+ ext = filepath.Ext(in)
+ )
+ if ext != ".png" && ext != ".icns" {
+ return fmt.Errorf("icon must be .icns or .png: %s", icon)
+ }
+ inf, err := os.Open(in)
+ if err != nil {
+ return errors.Wrap(err, "opening input file")
+ }
+ defer inf.Close()
+ src = inf
+ if ext == ".png" {
+ img, err := png.Decode(inf)
+ if err != nil {
+ return errors.Wrap(err, "decoding png")
+ }
+ buf := bytes.NewBuffer(nil)
+ if err := icns.Encode(buf, img); err != nil {
+ return errors.Wrap(err, "encoding icns")
+ }
+ src = buf
+ }
+ outf, err := os.Create(out)
+ if err != nil {
+ return errors.Wrap(err, "creating output file")
+ }
+ defer outf.Close()
+ if _, err := io.Copy(outf, src); err != nil {
+ return err
+ }
+ return nil
+}
+
+func omitEmpty(flag, value string) string {
+ if value == "" {
+ return ""
+ }
+ return fmt.Sprintf("%s=%s", flag, value)
+}
+
+// openZip returns the first file in the zip that contains the pattern.
+func openZip(u, dir, pattern string) (*bytes.Buffer, error) {
+ file, err := download(u, filepath.Join(dir, "ffmpeg.zip"))
+ if err != nil {
+ return nil, errors.Wrap(err, "downloading")
+ }
+ defer file.Close()
+ info, err := file.Stat()
+ if err != nil {
+ return nil, errors.Wrap(err, "getting file info")
+ }
+ zr, err := zip.NewReader(file, info.Size())
+ if err != nil {
+ return nil, errors.Wrap(err, "creating zip reader")
+ }
+ for _, zf := range zr.File {
+ if strings.Contains(zf.Name, pattern) {
+ var buf = bytes.NewBuffer(nil)
+ zfr, err := zf.Open()
+ if err != nil {
+ return nil, errors.Wrap(err, "reading zipped file")
+ }
+ defer zfr.Close()
+ if _, err := io.Copy(buf, zfr); err != nil {
+ return nil, errors.Wrap(err, "buffering zipped file")
+ }
+ return buf, nil
+ }
+ }
+ return nil, fmt.Errorf("no file found in zip for given pattern %q", pattern)
+}
+
+// download the file at the specified url.
+// If a file exists at dst, that file is returned.
+func download(u, dst string) (*os.File, error) {
+ dstf, err := os.Open(dst)
+ if err != nil && !os.IsNotExist(err) {
+ return nil, errors.Wrap(err, "opening downloaded file")
+ }
+ if err == nil {
+ return dstf, nil
+ }
+ resp, err := http.Get(u)
+ if err != nil {
+ return nil, errors.Wrap(err, "GET")
+ }
+ defer resp.Body.Close()
+ if !(resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices) {
+ return nil, fmt.Errorf("status %d", resp.StatusCode)
+ }
+ zipf, err := os.Create(dst)
+ if err != nil {
+ return nil, errors.Wrap(err, "creating destination file")
+ }
+ if _, err := io.Copy(zipf, resp.Body); err != nil {
+ return nil, errors.Wrap(err, "copying body")
+ }
+ return zipf, nil
+}
diff --git a/cmd/build/resolve.go b/cmd/build/resolve.go
@@ -0,0 +1,95 @@
+package main
+
+// Graph of Nodes that resolves dependencies.
+type Graph struct {
+ Nodes []Node
+ unresolved *NodeMap
+ resolved *NodeMap
+}
+
+// Append a node to the graph.
+func (g *Graph) Append(n Node) {
+ g.Nodes = append(g.Nodes, n)
+}
+
+// Resolve the graph.
+func (g *Graph) Resolve() []Node {
+ g.unresolved = &NodeMap{m: map[string]int{}}
+ g.resolved = &NodeMap{m: map[string]int{}}
+ for _, n := range g.Nodes {
+ g.resolve(n)
+ }
+ return g.resolved.List()
+}
+
+func (g *Graph) resolve(n Node) {
+ g.unresolved.Append(n)
+ defer g.resolved.Append(n)
+ defer g.unresolved.Remove(n)
+ for _, edge := range n.Requires() {
+ if _, resolved := g.resolved.Lookup(edge.ID()); !resolved {
+ if _, seen := g.unresolved.Lookup(edge.ID()); seen {
+ panic("Circular reference")
+ } else {
+ g.resolve(edge)
+ }
+ }
+ }
+}
+
+// Node on a graph.
+type Node interface {
+ ID() string
+ Requires() []Node
+}
+
+// NodeMap is a map of nodes.
+type NodeMap struct {
+ l []Node
+ m map[string]int
+}
+
+// List returns an ordered list of nodes.
+func (m *NodeMap) List() []Node {
+ return m.l
+}
+
+// Append a node.
+func (m *NodeMap) Append(n Node) {
+ m.l = append(m.l, n)
+ m.m[n.ID()] = len(m.l) - 1
+}
+
+// Remove a node.
+func (m *NodeMap) Remove(n Node) {
+ ii := m.m[n.ID()]
+ m.l = append(m.l[:ii], m.l[ii+1:]...)
+ delete(m.m, n.ID())
+}
+
+// Lookup a node for the given id.
+func (m *NodeMap) Lookup(id string) (Node, bool) {
+ ii, ok := m.m[id]
+ if !ok {
+ return nil, false
+ }
+ return m.l[ii], true
+}
+
+// taskNode wraps a Task to implement the Node interface.
+// Index allows us to return a Task object for a given Task name.
+type taskNode struct {
+ Task Task
+ Index map[string]Task
+}
+
+func (n taskNode) ID() string {
+ return n.Task.Name
+}
+
+func (n taskNode) Requires() (list []Node) {
+ for _, r := range n.Task.Requires {
+ list = append(list, taskNode{Task: n.Index[r], Index: n.Index})
+ }
+ return list
+}
diff --git a/cmd/build/task.go b/cmd/build/task.go
@@ -0,0 +1,86 @@
+package main
+
+import (
+ "fmt"
+ "sync"
+
+ "github.com/hashicorp/go-multierror"
+)
+
+// Task is an independent unit of work.
+type Task struct {
+ Name string
+ Op func() error
+ Requires []string
+}
+
+// FanOut executes all the independant tasks in parallel and waits for them to
+// finish.
+type FanOut []Task
+
+// Run all the tasks.
+func (tasks FanOut) Run() (err error) {
+ var (
+ failures = make(chan error)
+ wg = &sync.WaitGroup{}
+ // index contains all the Tasks and allows us to resolve a Task
+ // object from it's name alone.
+ // This is used for building the graph.
+ index = map[string]Task{}
+ // Each chain is a serialised list of tasks to execute.
+ // Chains can be executed independently because they do not
+ // share dependencies - thus parallelism.
+ chains [][]Task
+ )
+ for _, t := range tasks {
+ index[t.Name] = t
+ }
+ g := &Graph{}
+ for _, t := range tasks {
+ g.Append(taskNode{Task: t, Index: index})
+ }
+ resolved := g.Resolve()
+ // Since the resolved slice is an ordered list of tasks, we can slice it
+ // into independent chains delimited by tasks that have no dependencies.
+ for _, n := range resolved {
+ if len(n.Requires()) > 0 {
+ // Append to the current chain.
+ current := len(chains) - 1
+ chains[current] = append(chains[current], index[n.ID()])
+ } else {
+ // Start a new chain.
+ chains = append(chains, []Task{index[n.ID()]})
+ }
+ }
+ wg.Add(len(chains))
+ for _, chain := range chains {
+ chain := chain
+ go func() {
+ defer wg.Done()
+ for _, t := range chain {
+ fmt.Printf("run: %v\n", t.Name)
+ if err := t.Op(); err != nil {
+ failures <- TaskError{Task: t, Err: err}
+ }
+ }
+ }()
+ }
+ go func() {
+ wg.Wait()
+ close(failures)
+ }()
+ for failure := range failures {
+ err = multierror.Append(err, failure)
+ }
+ return err
+}
+
+// TaskError associates an error with the task that produced it.
+type TaskError struct {
+ Task Task
+ Err error
+}
+
+func (err TaskError) Error() string {
+ return fmt.Sprintf("task %q: %v", err.Task.Name, err.Err)
+}