123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- // 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
- }
|