github.com/timstclair/heapster@v0.20.0-alpha1/Godeps/_workspace/src/google.golang.org/appengine/cmd/aebundler/aebundler.go (about)

     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
    14  package main
    15  
    16  import (
    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  
    31  var (
    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 a Managed VM app")
    35  
    36  	skipFiles = map[string]bool{
    37  		".git":        true,
    38  		".gitconfig":  true,
    39  		".hg":         true,
    40  		".travis.yml": true,
    41  	}
    42  )
    43  
    44  const (
    45  	newMain = `package main
    46  import "google.golang.org/appengine"
    47  func main() {
    48  	appengine.Main()
    49  }
    50  `
    51  )
    52  
    53  func 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  
    60  func 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.
    87  func errorf(format string, a ...interface{}) {
    88  	fmt.Fprintf(os.Stderr, "aebundler: "+format+"\n", a...)
    89  	os.Exit(1)
    90  }
    91  
    92  type 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.
   100  func 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.
   116  func 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).
   128  func (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.
   168  func 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.
   199  func 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.
   228  func 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.
   239  func 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.
   265  func 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.
   293  func 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.
   316  func 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.
   322  func 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  }