1// Copyright 2015 Google Inc. All rights reserved. 2// Use of this source code is governed by the Apache 2.0 3// license that can be found in the LICENSE file. 4 5// Program aebundler turns a Go app into a fully self-contained tar file. 6// The app and its subdirectories (if any) are placed under "." 7// and the dependencies from $GOPATH are placed under ./_gopath/src. 8// A main func is synthesized if one does not exist. 9// 10// A sample Dockerfile to be used with this bundler could look like this: 11// FROM gcr.io/google-appengine/go-compat 12// ADD . /app 13// RUN GOPATH=/app/_gopath go build -tags appenginevm -o /app/_ah/exe 14package main 15 16import ( 17 "archive/tar" 18 "flag" 19 "fmt" 20 "go/ast" 21 "go/build" 22 "go/parser" 23 "go/token" 24 "io" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "strings" 29) 30 31var ( 32 output = flag.String("o", "", "name of output tar file or '-' for stdout") 33 rootDir = flag.String("root", ".", "directory name of application root") 34 vm = flag.Bool("vm", true, `bundle an app for App Engine "flexible environment"`) 35 36 skipFiles = map[string]bool{ 37 ".git": true, 38 ".gitconfig": true, 39 ".hg": true, 40 ".travis.yml": true, 41 } 42) 43 44const ( 45 newMain = `package main 46import "google.golang.org/appengine" 47func main() { 48 appengine.Main() 49} 50` 51) 52 53func usage() { 54 fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 55 fmt.Fprintf(os.Stderr, "\t%s -o <file.tar|->\tBundle app to named tar file or stdout\n", os.Args[0]) 56 fmt.Fprintf(os.Stderr, "\noptional arguments:\n") 57 flag.PrintDefaults() 58} 59 60func main() { 61 flag.Usage = usage 62 flag.Parse() 63 64 var tags []string 65 if *vm { 66 tags = append(tags, "appenginevm") 67 } else { 68 tags = append(tags, "appengine") 69 } 70 71 tarFile := *output 72 if tarFile == "" { 73 usage() 74 errorf("Required -o flag not specified.") 75 } 76 77 app, err := analyze(tags) 78 if err != nil { 79 errorf("Error analyzing app: %v", err) 80 } 81 if err := app.bundle(tarFile); err != nil { 82 errorf("Unable to bundle app: %v", err) 83 } 84} 85 86// errorf prints the error message and exits. 87func errorf(format string, a ...interface{}) { 88 fmt.Fprintf(os.Stderr, "aebundler: "+format+"\n", a...) 89 os.Exit(1) 90} 91 92type app struct { 93 hasMain bool 94 appFiles []string 95 imports map[string]string 96} 97 98// analyze checks the app for building with the given build tags and returns hasMain, 99// app files, and a map of full directory import names to original import names. 100func analyze(tags []string) (*app, error) { 101 ctxt := buildContext(tags) 102 hasMain, appFiles, err := checkMain(ctxt) 103 if err != nil { 104 return nil, err 105 } 106 gopath := filepath.SplitList(ctxt.GOPATH) 107 im, err := imports(ctxt, *rootDir, gopath) 108 return &app{ 109 hasMain: hasMain, 110 appFiles: appFiles, 111 imports: im, 112 }, err 113} 114 115// buildContext returns the context for building the source. 116func buildContext(tags []string) *build.Context { 117 return &build.Context{ 118 GOARCH: build.Default.GOARCH, 119 GOOS: build.Default.GOOS, 120 GOROOT: build.Default.GOROOT, 121 GOPATH: build.Default.GOPATH, 122 Compiler: build.Default.Compiler, 123 BuildTags: append(build.Default.BuildTags, tags...), 124 } 125} 126 127// bundle bundles the app into the named tarFile ("-"==stdout). 128func (s *app) bundle(tarFile string) (err error) { 129 var out io.Writer 130 if tarFile == "-" { 131 out = os.Stdout 132 } else { 133 f, err := os.Create(tarFile) 134 if err != nil { 135 return err 136 } 137 defer func() { 138 if cerr := f.Close(); err == nil { 139 err = cerr 140 } 141 }() 142 out = f 143 } 144 tw := tar.NewWriter(out) 145 146 for srcDir, importName := range s.imports { 147 dstDir := "_gopath/src/" + importName 148 if err = copyTree(tw, dstDir, srcDir); err != nil { 149 return fmt.Errorf("unable to copy directory %v to %v: %v", srcDir, dstDir, err) 150 } 151 } 152 if err := copyTree(tw, ".", *rootDir); err != nil { 153 return fmt.Errorf("unable to copy root directory to /app: %v", err) 154 } 155 if !s.hasMain { 156 if err := synthesizeMain(tw, s.appFiles); err != nil { 157 return fmt.Errorf("unable to synthesize new main func: %v", err) 158 } 159 } 160 161 if err := tw.Close(); err != nil { 162 return fmt.Errorf("unable to close tar file %v: %v", tarFile, err) 163 } 164 return nil 165} 166 167// synthesizeMain generates a new main func and writes it to the tarball. 168func synthesizeMain(tw *tar.Writer, appFiles []string) error { 169 appMap := make(map[string]bool) 170 for _, f := range appFiles { 171 appMap[f] = true 172 } 173 var f string 174 for i := 0; i < 100; i++ { 175 f = fmt.Sprintf("app_main%d.go", i) 176 if !appMap[filepath.Join(*rootDir, f)] { 177 break 178 } 179 } 180 if appMap[filepath.Join(*rootDir, f)] { 181 return fmt.Errorf("unable to find unique name for %v", f) 182 } 183 hdr := &tar.Header{ 184 Name: f, 185 Mode: 0644, 186 Size: int64(len(newMain)), 187 } 188 if err := tw.WriteHeader(hdr); err != nil { 189 return fmt.Errorf("unable to write header for %v: %v", f, err) 190 } 191 if _, err := tw.Write([]byte(newMain)); err != nil { 192 return fmt.Errorf("unable to write %v to tar file: %v", f, err) 193 } 194 return nil 195} 196 197// imports returns a map of all import directories (recursively) used by the app. 198// The return value maps full directory names to original import names. 199func imports(ctxt *build.Context, srcDir string, gopath []string) (map[string]string, error) { 200 pkg, err := ctxt.ImportDir(srcDir, 0) 201 if err != nil { 202 return nil, fmt.Errorf("unable to analyze source: %v", err) 203 } 204 205 // Resolve all non-standard-library imports 206 result := make(map[string]string) 207 for _, v := range pkg.Imports { 208 if !strings.Contains(v, ".") { 209 continue 210 } 211 src, err := findInGopath(v, gopath) 212 if err != nil { 213 return nil, fmt.Errorf("unable to find import %v in gopath %v: %v", v, gopath, err) 214 } 215 result[src] = v 216 im, err := imports(ctxt, src, gopath) 217 if err != nil { 218 return nil, fmt.Errorf("unable to parse package %v: %v", src, err) 219 } 220 for k, v := range im { 221 result[k] = v 222 } 223 } 224 return result, nil 225} 226 227// findInGopath searches the gopath for the named import directory. 228func findInGopath(dir string, gopath []string) (string, error) { 229 for _, v := range gopath { 230 dst := filepath.Join(v, "src", dir) 231 if _, err := os.Stat(dst); err == nil { 232 return dst, nil 233 } 234 } 235 return "", fmt.Errorf("unable to find package %v in gopath %v", dir, gopath) 236} 237 238// copyTree copies srcDir to tar file dstDir, ignoring skipFiles. 239func copyTree(tw *tar.Writer, dstDir, srcDir string) error { 240 entries, err := ioutil.ReadDir(srcDir) 241 if err != nil { 242 return fmt.Errorf("unable to read dir %v: %v", srcDir, err) 243 } 244 for _, entry := range entries { 245 n := entry.Name() 246 if skipFiles[n] { 247 continue 248 } 249 s := filepath.Join(srcDir, n) 250 d := filepath.Join(dstDir, n) 251 if entry.IsDir() { 252 if err := copyTree(tw, d, s); err != nil { 253 return fmt.Errorf("unable to copy dir %v to %v: %v", s, d, err) 254 } 255 continue 256 } 257 if err := copyFile(tw, d, s); err != nil { 258 return fmt.Errorf("unable to copy dir %v to %v: %v", s, d, err) 259 } 260 } 261 return nil 262} 263 264// copyFile copies src to tar file dst. 265func copyFile(tw *tar.Writer, dst, src string) error { 266 s, err := os.Open(src) 267 if err != nil { 268 return fmt.Errorf("unable to open %v: %v", src, err) 269 } 270 defer s.Close() 271 fi, err := s.Stat() 272 if err != nil { 273 return fmt.Errorf("unable to stat %v: %v", src, err) 274 } 275 276 hdr, err := tar.FileInfoHeader(fi, dst) 277 if err != nil { 278 return fmt.Errorf("unable to create tar header for %v: %v", dst, err) 279 } 280 hdr.Name = dst 281 if err := tw.WriteHeader(hdr); err != nil { 282 return fmt.Errorf("unable to write header for %v: %v", dst, err) 283 } 284 _, err = io.Copy(tw, s) 285 if err != nil { 286 return fmt.Errorf("unable to copy %v to %v: %v", src, dst, err) 287 } 288 return nil 289} 290 291// checkMain verifies that there is a single "main" function. 292// It also returns a list of all Go source files in the app. 293func checkMain(ctxt *build.Context) (bool, []string, error) { 294 pkg, err := ctxt.ImportDir(*rootDir, 0) 295 if err != nil { 296 return false, nil, fmt.Errorf("unable to analyze source: %v", err) 297 } 298 if !pkg.IsCommand() { 299 errorf("Your app's package needs to be changed from %q to \"main\".\n", pkg.Name) 300 } 301 // Search for a "func main" 302 var hasMain bool 303 var appFiles []string 304 for _, f := range pkg.GoFiles { 305 n := filepath.Join(*rootDir, f) 306 appFiles = append(appFiles, n) 307 if hasMain, err = readFile(n); err != nil { 308 return false, nil, fmt.Errorf("error parsing %q: %v", n, err) 309 } 310 } 311 return hasMain, appFiles, nil 312} 313 314// isMain returns whether the given function declaration is a main function. 315// Such a function must be called "main", not have a receiver, and have no arguments or return types. 316func isMain(f *ast.FuncDecl) bool { 317 ft := f.Type 318 return f.Name.Name == "main" && f.Recv == nil && ft.Params.NumFields() == 0 && ft.Results.NumFields() == 0 319} 320 321// readFile reads and parses the Go source code file and returns whether it has a main function. 322func readFile(filename string) (hasMain bool, err error) { 323 var src []byte 324 src, err = ioutil.ReadFile(filename) 325 if err != nil { 326 return 327 } 328 fset := token.NewFileSet() 329 file, err := parser.ParseFile(fset, filename, src, 0) 330 for _, decl := range file.Decls { 331 funcDecl, ok := decl.(*ast.FuncDecl) 332 if !ok { 333 continue 334 } 335 if !isMain(funcDecl) { 336 continue 337 } 338 hasMain = true 339 break 340 } 341 return 342} 343