giffer

Create .gif images from sites like youtube.com
Log | Files | Refs | LICENSE

darwin.go (6003B)


      1 package main
      2 
      3 import (
      4 	"encoding/binary"
      5 	"fmt"
      6 	"io"
      7 	"io/ioutil"
      8 	"os"
      9 	"os/exec"
     10 	"os/user"
     11 	"path/filepath"
     12 	"strings"
     13 
     14 	"github.com/pkg/errors"
     15 )
     16 
     17 const plist = `
     18 <?xml version="1.0" encoding="UTF-8"?>
     19 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
     20 <plist version="1.0">
     21 <dict>
     22 	<key>CFBundleExecutable</key>
     23 		<string>giffer.sh</string>
     24 	<key>CFBundleIconFile</key>
     25 		<string>icon.icns</string>
     26 	<key>CFBundleIdentifier</key>
     27 		<string>com.jackmordaunt.giffer</string>
     28 	<key>NSHighResolutionCapable</key>
     29 		<true/>
     30 	<key>NSAppTransportSecurity</key>
     31 		<dict>
     32 			<key>NSExceptionDomains</key>
     33 				<dict>
     34 					<key>localhost</key>
     35 						<dict>
     36 							<key>NSExceptionAllowsInsecureHTTPLoads</key>
     37 								<true/>
     38 							<key>NSIncludesSubdomains</key>
     39 								<true/>
     40 						</dict>
     41 				</dict>
     42 		</dict>
     43 </dict>
     44 </plist>
     45 `
     46 
     47 // Darwin builds Giffer as a .app for MacOS.
     48 type Darwin struct {
     49 	// Bin is the name of the built binary.
     50 	Bin string
     51 	// Pkg is the go package import string ie,
     52 	// "github.com/jackmordaunt/giffer/desktop".
     53 	Pkg string
     54 	// UI is the root path to the UI files.
     55 	UI string
     56 	// FFmpeg is the URL or filepath to a static FFmpeg binary.
     57 	FFmpeg string
     58 	// Icon is the path to the icon file (.icns or .png).
     59 	Icon string
     60 	// BuildTags to pass to go build.
     61 	BuildTags string
     62 }
     63 
     64 // Build executes the build and writes the result to dist.
     65 func (app *Darwin) Build(dist string) error {
     66 	var (
     67 		contents  = filepath.Join(dist, "Giffer.app", "Contents")
     68 		macos     = filepath.Join(dist, "Giffer.app", "Contents", "MacOS")
     69 		resources = filepath.Join(dist, "Giffer.app", "Contents", "Resources")
     70 		info      = filepath.Join(dist, "Giffer.app", "Contents", "Info.plist")
     71 		iconf     = filepath.Join(dist, "Giffer.app", "Contents", "Resources", "icon.icns")
     72 	)
     73 	for _, dir := range []string{contents, macos, resources} {
     74 		if err := os.MkdirAll(dir, 0755); err != nil && !os.IsExist(err) {
     75 			return errors.Wrap(err, "preparing directory")
     76 		}
     77 	}
     78 	tasks := FanOut([]Task{
     79 		{
     80 			Name: "compile ui",
     81 			Op: func() error {
     82 				ui, err := filepath.Abs(app.UI)
     83 				if err != nil {
     84 					return errors.Wrap(err, "resolving absolute path to UI files")
     85 				}
     86 				app.UI = ui
     87 				var (
     88 					src          = filepath.Join(app.UI, "src")
     89 					checksumFile = filepath.Join(app.UI, "checksum")
     90 				)
     91 				h, err := Hash(src)
     92 				if err != nil {
     93 					return errors.Wrap(err, "hashing ui files")
     94 				}
     95 				checksum, err := ioutil.ReadFile(checksumFile)
     96 				if err != nil && !os.IsNotExist(err) {
     97 					return errors.Wrap(err, "reading checksum file")
     98 				}
     99 				if !os.IsNotExist(err) && h == binary.LittleEndian.Uint64(checksum) {
    100 					return nil
    101 				}
    102 				cmd := exec.Command("yarn", "build")
    103 				cmd.Dir = app.UI
    104 				if out, err := cmd.CombinedOutput(); err != nil {
    105 					return errors.Wrap(err, string(out))
    106 				}
    107 				buf := make([]byte, 64)
    108 				binary.LittleEndian.PutUint64(buf, h)
    109 				return ioutil.WriteFile(checksumFile, buf, 0644)
    110 			},
    111 		},
    112 		{
    113 			Name: "write wrapper script",
    114 			Op: func() error {
    115 				wrapper := strings.Join([]string{
    116 					"#!/usr/bin/env bash\n",
    117 					"DIR=$(cd \"$(dirname \"$0\")\"; pwd)\n",
    118 					fmt.Sprintf("$DIR/%s ", app.Bin),
    119 					"-v ",
    120 					"-ffmpeg $DIR/../Resources/ffmpeg ",
    121 					fmt.Sprintf("-log $DIR/../%s.log ", app.Bin),
    122 					"-tmp $DIR/../Resources/tmp ",
    123 				}, "")
    124 				name := filepath.Join(macos, fmt.Sprintf("%s.sh", app.Bin))
    125 				if err := ioutil.WriteFile(name, []byte(wrapper), 0777); err != nil {
    126 					return errors.Wrap(err, "writing wrapper file")
    127 				}
    128 				return nil
    129 			},
    130 		},
    131 		{
    132 			Name:     "compile binary",
    133 			Requires: []string{"compile ui"},
    134 			Op: func() error {
    135 				args := []string{"go", "build"}
    136 				if app.BuildTags != "" {
    137 					args = append(args, "-tags", app.BuildTags)
    138 				}
    139 				args = append(args, []string{
    140 					"-o", filepath.Join(macos, app.Bin),
    141 					app.Pkg,
    142 				}...)
    143 				cmd := exec.Command(args[0], args[1:]...)
    144 				if out, err := cmd.CombinedOutput(); err != nil {
    145 					return errors.Wrap(err, string(out))
    146 				}
    147 				return nil
    148 			},
    149 		},
    150 		{
    151 			Name:     "embed resources",
    152 			Requires: []string{"compile binary"},
    153 			Op: func() error {
    154 				cmd := exec.Command(
    155 					"rice",
    156 					"append",
    157 					"-i", app.Pkg,
    158 					"--exec", filepath.Join(macos, app.Bin),
    159 				)
    160 				if out, err := cmd.CombinedOutput(); err != nil {
    161 					return errors.Wrap(err, string(out))
    162 				}
    163 				return nil
    164 			},
    165 		},
    166 		{
    167 			Name: "create icon.icns",
    168 			Op: func() error {
    169 				if app.Icon != "" {
    170 					if err := WriteIcon(app.Icon, iconf); err != nil {
    171 						return errors.Wrap(err, "writing icon")
    172 					}
    173 				}
    174 				return nil
    175 			},
    176 		},
    177 		{
    178 			Name: "create Info.plist",
    179 			Op: func() error {
    180 				if err := ioutil.WriteFile(info, []byte(plist), 0644); err != nil {
    181 					return errors.Wrap(err, "writing plist")
    182 				}
    183 				return nil
    184 			},
    185 		},
    186 		{
    187 			Name: "bundle ffmpeg",
    188 			Op: func() error {
    189 				var (
    190 					src io.Reader
    191 					err error
    192 				)
    193 				if app.FFmpeg == "" {
    194 					app.FFmpeg = "https://ffmpeg.zeranoe.com/builds/macos64/static/ffmpeg-latest-macos64-static.zip"
    195 				}
    196 				if strings.Contains(app.FFmpeg, "https://") {
    197 					u, err := user.Current()
    198 					if err != nil {
    199 						return errors.Wrap(err, "determine current user")
    200 					}
    201 					downloads := filepath.Join(u.HomeDir, "Downloads")
    202 					src, err = openZip(app.FFmpeg, downloads, "bin/ffmpeg")
    203 					if err != nil {
    204 						return errors.Wrap(err, "downloading ffmpeg")
    205 					}
    206 				} else {
    207 					srcf, err := os.Open(app.FFmpeg)
    208 					if err != nil {
    209 						return errors.Wrap(err, "opening ffmpeg file")
    210 					}
    211 					defer srcf.Close()
    212 					src = srcf
    213 				}
    214 				destf, err := os.OpenFile(
    215 					filepath.Join(resources, "ffmpeg"),
    216 					os.O_CREATE|os.O_RDWR,
    217 					0777)
    218 				if err != nil {
    219 					return errors.Wrap(err, "opening desintaiton file")
    220 				}
    221 				defer destf.Close()
    222 				if _, err := io.Copy(destf, src); err != nil {
    223 					return errors.Wrap(err, "copying")
    224 				}
    225 				return nil
    226 			},
    227 		},
    228 	})
    229 	return tasks.Run()
    230 }