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