| // Copyright 2015 Google Inc. All rights reserved. |
| // Use of this source code is governed by the Apache 2.0 |
| // license that can be found in the LICENSE file. |
| |
| // Program aebundler turns a Go app into a fully self-contained tar file. |
| // The app and its subdirectories (if any) are placed under "." |
| // and the dependencies from $GOPATH are placed under ./_gopath/src. |
| // A main func is synthesized if one does not exist. |
| // |
| // A sample Dockerfile to be used with this bundler could look like this: |
| // FROM gcr.io/google_appengine/go-compat |
| // ADD . /app |
| // RUN GOPATH=/app/_gopath go build -tags appenginevm -o /app/_ah/exe |
| package main |
| |
| import ( |
| "archive/tar" |
| "flag" |
| "fmt" |
| "go/ast" |
| "go/build" |
| "go/parser" |
| "go/token" |
| "io" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "strings" |
| ) |
| |
| var ( |
| output = flag.String("o", "", "name of output tar file or '-' for stdout") |
| rootDir = flag.String("root", ".", "directory name of application root") |
| vm = flag.Bool("vm", true, "bundle a Managed VM app") |
| |
| skipFiles = map[string]bool{ |
| ".git": true, |
| ".gitconfig": true, |
| ".hg": true, |
| ".travis.yml": true, |
| } |
| ) |
| |
| const ( |
| newMain = `package main |
| import "google.golang.org/appengine" |
| func main() { |
| appengine.Main() |
| } |
| ` |
| ) |
| |
| func usage() { |
| fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) |
| fmt.Fprintf(os.Stderr, "\t%s -o <file.tar|->\tBundle app to named tar file or stdout\n", os.Args[0]) |
| fmt.Fprintf(os.Stderr, "\noptional arguments:\n") |
| flag.PrintDefaults() |
| } |
| |
| func main() { |
| flag.Usage = usage |
| flag.Parse() |
| |
| var tags []string |
| if *vm { |
| tags = append(tags, "appenginevm") |
| } else { |
| tags = append(tags, "appengine") |
| } |
| |
| tarFile := *output |
| if tarFile == "" { |
| usage() |
| errorf("Required -o flag not specified.") |
| } |
| |
| app, err := analyze(tags) |
| if err != nil { |
| errorf("Error analyzing app: %v", err) |
| } |
| if err := app.bundle(tarFile); err != nil { |
| errorf("Unable to bundle app: %v", err) |
| } |
| } |
| |
| // errorf prints the error message and exits. |
| func errorf(format string, a ...interface{}) { |
| fmt.Fprintf(os.Stderr, "aebundler: "+format+"\n", a...) |
| os.Exit(1) |
| } |
| |
| type app struct { |
| hasMain bool |
| appFiles []string |
| imports map[string]string |
| } |
| |
| // analyze checks the app for building with the given build tags and returns hasMain, |
| // app files, and a map of full directory import names to original import names. |
| func analyze(tags []string) (*app, error) { |
| ctxt := buildContext(tags) |
| hasMain, appFiles, err := checkMain(ctxt) |
| if err != nil { |
| return nil, err |
| } |
| gopath := filepath.SplitList(ctxt.GOPATH) |
| im, err := imports(ctxt, *rootDir, gopath) |
| return &app{ |
| hasMain: hasMain, |
| appFiles: appFiles, |
| imports: im, |
| }, err |
| } |
| |
| // buildContext returns the context for building the source. |
| func buildContext(tags []string) *build.Context { |
| return &build.Context{ |
| GOARCH: build.Default.GOARCH, |
| GOOS: build.Default.GOOS, |
| GOROOT: build.Default.GOROOT, |
| GOPATH: build.Default.GOPATH, |
| Compiler: build.Default.Compiler, |
| BuildTags: append(build.Default.BuildTags, tags...), |
| } |
| } |
| |
| // bundle bundles the app into the named tarFile ("-"==stdout). |
| func (s *app) bundle(tarFile string) (err error) { |
| var out io.Writer |
| if tarFile == "-" { |
| out = os.Stdout |
| } else { |
| f, err := os.Create(tarFile) |
| if err != nil { |
| return err |
| } |
| defer func() { |
| if cerr := f.Close(); err == nil { |
| err = cerr |
| } |
| }() |
| out = f |
| } |
| tw := tar.NewWriter(out) |
| |
| for srcDir, importName := range s.imports { |
| dstDir := "_gopath/src/" + importName |
| if err = copyTree(tw, dstDir, srcDir); err != nil { |
| return fmt.Errorf("unable to copy directory %v to %v: %v", srcDir, dstDir, err) |
| } |
| } |
| if err := copyTree(tw, ".", *rootDir); err != nil { |
| return fmt.Errorf("unable to copy root directory to /app: %v", err) |
| } |
| if !s.hasMain { |
| if err := synthesizeMain(tw, s.appFiles); err != nil { |
| return fmt.Errorf("unable to synthesize new main func: %v", err) |
| } |
| } |
| |
| if err := tw.Close(); err != nil { |
| return fmt.Errorf("unable to close tar file %v: %v", tarFile, err) |
| } |
| return nil |
| } |
| |
| // synthesizeMain generates a new main func and writes it to the tarball. |
| func synthesizeMain(tw *tar.Writer, appFiles []string) error { |
| appMap := make(map[string]bool) |
| for _, f := range appFiles { |
| appMap[f] = true |
| } |
| var f string |
| for i := 0; i < 100; i++ { |
| f = fmt.Sprintf("app_main%d.go", i) |
| if !appMap[filepath.Join(*rootDir, f)] { |
| break |
| } |
| } |
| if appMap[filepath.Join(*rootDir, f)] { |
| return fmt.Errorf("unable to find unique name for %v", f) |
| } |
| hdr := &tar.Header{ |
| Name: f, |
| Mode: 0644, |
| Size: int64(len(newMain)), |
| } |
| if err := tw.WriteHeader(hdr); err != nil { |
| return fmt.Errorf("unable to write header for %v: %v", f, err) |
| } |
| if _, err := tw.Write([]byte(newMain)); err != nil { |
| return fmt.Errorf("unable to write %v to tar file: %v", f, err) |
| } |
| return nil |
| } |
| |
| // imports returns a map of all import directories (recursively) used by the app. |
| // The return value maps full directory names to original import names. |
| func imports(ctxt *build.Context, srcDir string, gopath []string) (map[string]string, error) { |
| pkg, err := ctxt.ImportDir(srcDir, 0) |
| if err != nil { |
| return nil, fmt.Errorf("unable to analyze source: %v", err) |
| } |
| |
| // Resolve all non-standard-library imports |
| result := make(map[string]string) |
| for _, v := range pkg.Imports { |
| if !strings.Contains(v, ".") { |
| continue |
| } |
| src, err := findInGopath(v, gopath) |
| if err != nil { |
| return nil, fmt.Errorf("unable to find import %v in gopath %v: %v", v, gopath, err) |
| } |
| result[src] = v |
| im, err := imports(ctxt, src, gopath) |
| if err != nil { |
| return nil, fmt.Errorf("unable to parse package %v: %v", src, err) |
| } |
| for k, v := range im { |
| result[k] = v |
| } |
| } |
| return result, nil |
| } |
| |
| // findInGopath searches the gopath for the named import directory. |
| func findInGopath(dir string, gopath []string) (string, error) { |
| for _, v := range gopath { |
| dst := filepath.Join(v, "src", dir) |
| if _, err := os.Stat(dst); err == nil { |
| return dst, nil |
| } |
| } |
| return "", fmt.Errorf("unable to find package %v in gopath %v", dir, gopath) |
| } |
| |
| // copyTree copies srcDir to tar file dstDir, ignoring skipFiles. |
| func copyTree(tw *tar.Writer, dstDir, srcDir string) error { |
| entries, err := ioutil.ReadDir(srcDir) |
| if err != nil { |
| return fmt.Errorf("unable to read dir %v: %v", srcDir, err) |
| } |
| for _, entry := range entries { |
| n := entry.Name() |
| if skipFiles[n] { |
| continue |
| } |
| s := filepath.Join(srcDir, n) |
| d := filepath.Join(dstDir, n) |
| if entry.IsDir() { |
| if err := copyTree(tw, d, s); err != nil { |
| return fmt.Errorf("unable to copy dir %v to %v: %v", s, d, err) |
| } |
| continue |
| } |
| if err := copyFile(tw, d, s); err != nil { |
| return fmt.Errorf("unable to copy dir %v to %v: %v", s, d, err) |
| } |
| } |
| return nil |
| } |
| |
| // copyFile copies src to tar file dst. |
| func copyFile(tw *tar.Writer, dst, src string) error { |
| s, err := os.Open(src) |
| if err != nil { |
| return fmt.Errorf("unable to open %v: %v", src, err) |
| } |
| defer s.Close() |
| fi, err := s.Stat() |
| if err != nil { |
| return fmt.Errorf("unable to stat %v: %v", src, err) |
| } |
| |
| hdr, err := tar.FileInfoHeader(fi, dst) |
| if err != nil { |
| return fmt.Errorf("unable to create tar header for %v: %v", dst, err) |
| } |
| hdr.Name = dst |
| if err := tw.WriteHeader(hdr); err != nil { |
| return fmt.Errorf("unable to write header for %v: %v", dst, err) |
| } |
| _, err = io.Copy(tw, s) |
| if err != nil { |
| return fmt.Errorf("unable to copy %v to %v: %v", src, dst, err) |
| } |
| return nil |
| } |
| |
| // checkMain verifies that there is a single "main" function. |
| // It also returns a list of all Go source files in the app. |
| func checkMain(ctxt *build.Context) (bool, []string, error) { |
| pkg, err := ctxt.ImportDir(*rootDir, 0) |
| if err != nil { |
| return false, nil, fmt.Errorf("unable to analyze source: %v", err) |
| } |
| if !pkg.IsCommand() { |
| errorf("Your app's package needs to be changed from %q to \"main\".\n", pkg.Name) |
| } |
| // Search for a "func main" |
| var hasMain bool |
| var appFiles []string |
| for _, f := range pkg.GoFiles { |
| n := filepath.Join(*rootDir, f) |
| appFiles = append(appFiles, n) |
| if hasMain, err = readFile(n); err != nil { |
| return false, nil, fmt.Errorf("error parsing %q: %v", n, err) |
| } |
| } |
| return hasMain, appFiles, nil |
| } |
| |
| // isMain returns whether the given function declaration is a main function. |
| // Such a function must be called "main", not have a receiver, and have no arguments or return types. |
| func isMain(f *ast.FuncDecl) bool { |
| ft := f.Type |
| return f.Name.Name == "main" && f.Recv == nil && ft.Params.NumFields() == 0 && ft.Results.NumFields() == 0 |
| } |
| |
| // readFile reads and parses the Go source code file and returns whether it has a main function. |
| func readFile(filename string) (hasMain bool, err error) { |
| var src []byte |
| src, err = ioutil.ReadFile(filename) |
| if err != nil { |
| return |
| } |
| fset := token.NewFileSet() |
| file, err := parser.ParseFile(fset, filename, src, 0) |
| for _, decl := range file.Decls { |
| funcDecl, ok := decl.(*ast.FuncDecl) |
| if !ok { |
| continue |
| } |
| if !isMain(funcDecl) { |
| continue |
| } |
| hasMain = true |
| break |
| } |
| return |
| } |