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 }