blob: c66849e8337a09598a2670fcd45c5bd68570fa5e [file] [log] [blame]
// 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 an app for App Engine "flexible environment"`)
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
}