339 lines
8.7 KiB
Go
339 lines
8.7 KiB
Go
// 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
|
|
}
|