aebundler.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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. // Program aebundler turns a Go app into a fully self-contained tar file.
  5. // The app and its subdirectories (if any) are placed under "."
  6. // and the dependencies from $GOPATH are placed under ./_gopath/src.
  7. // A main func is synthesized if one does not exist.
  8. //
  9. // A sample Dockerfile to be used with this bundler could look like this:
  10. // FROM gcr.io/google_appengine/go-compat
  11. // ADD . /app
  12. // RUN GOPATH=/app/_gopath go build -tags appenginevm -o /app/_ah/exe
  13. package main
  14. import (
  15. "archive/tar"
  16. "flag"
  17. "fmt"
  18. "go/ast"
  19. "go/build"
  20. "go/parser"
  21. "go/token"
  22. "io"
  23. "io/ioutil"
  24. "os"
  25. "path/filepath"
  26. "strings"
  27. )
  28. var (
  29. output = flag.String("o", "", "name of output tar file or '-' for stdout")
  30. rootDir = flag.String("root", ".", "directory name of application root")
  31. vm = flag.Bool("vm", true, "bundle a Managed VM app")
  32. skipFiles = map[string]bool{
  33. ".git": true,
  34. ".gitconfig": true,
  35. ".hg": true,
  36. ".travis.yml": true,
  37. }
  38. )
  39. const (
  40. newMain = `package main
  41. import "google.golang.org/appengine"
  42. func main() {
  43. appengine.Main()
  44. }
  45. `
  46. )
  47. func usage() {
  48. fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
  49. fmt.Fprintf(os.Stderr, "\t%s -o <file.tar|->\tBundle app to named tar file or stdout\n", os.Args[0])
  50. fmt.Fprintf(os.Stderr, "\noptional arguments:\n")
  51. flag.PrintDefaults()
  52. }
  53. func main() {
  54. flag.Usage = usage
  55. flag.Parse()
  56. var tags []string
  57. if *vm {
  58. tags = append(tags, "appenginevm")
  59. } else {
  60. tags = append(tags, "appengine")
  61. }
  62. tarFile := *output
  63. if tarFile == "" {
  64. usage()
  65. errorf("Required -o flag not specified.")
  66. }
  67. app, err := analyze(tags)
  68. if err != nil {
  69. errorf("Error analyzing app: %v", err)
  70. }
  71. if err := app.bundle(tarFile); err != nil {
  72. errorf("Unable to bundle app: %v", err)
  73. }
  74. }
  75. // errorf prints the error message and exits.
  76. func errorf(format string, a ...interface{}) {
  77. fmt.Fprintf(os.Stderr, "aebundler: "+format+"\n", a...)
  78. os.Exit(1)
  79. }
  80. type app struct {
  81. hasMain bool
  82. appFiles []string
  83. imports map[string]string
  84. }
  85. // analyze checks the app for building with the given build tags and returns hasMain,
  86. // app files, and a map of full directory import names to original import names.
  87. func analyze(tags []string) (*app, error) {
  88. ctxt := buildContext(tags)
  89. hasMain, appFiles, err := checkMain(ctxt)
  90. if err != nil {
  91. return nil, err
  92. }
  93. gopath := filepath.SplitList(ctxt.GOPATH)
  94. im, err := imports(ctxt, *rootDir, gopath)
  95. return &app{
  96. hasMain: hasMain,
  97. appFiles: appFiles,
  98. imports: im,
  99. }, err
  100. }
  101. // buildContext returns the context for building the source.
  102. func buildContext(tags []string) *build.Context {
  103. return &build.Context{
  104. GOARCH: build.Default.GOARCH,
  105. GOOS: build.Default.GOOS,
  106. GOROOT: build.Default.GOROOT,
  107. GOPATH: build.Default.GOPATH,
  108. Compiler: build.Default.Compiler,
  109. BuildTags: append(build.Default.BuildTags, tags...),
  110. }
  111. }
  112. // bundle bundles the app into the named tarFile ("-"==stdout).
  113. func (s *app) bundle(tarFile string) (err error) {
  114. var out io.Writer
  115. if tarFile == "-" {
  116. out = os.Stdout
  117. } else {
  118. f, err := os.Create(tarFile)
  119. if err != nil {
  120. return err
  121. }
  122. defer func() {
  123. if cerr := f.Close(); err == nil {
  124. err = cerr
  125. }
  126. }()
  127. out = f
  128. }
  129. tw := tar.NewWriter(out)
  130. for srcDir, importName := range s.imports {
  131. dstDir := "_gopath/src/" + importName
  132. if err = copyTree(tw, dstDir, srcDir); err != nil {
  133. return fmt.Errorf("unable to copy directory %v to %v: %v", srcDir, dstDir, err)
  134. }
  135. }
  136. if err := copyTree(tw, ".", *rootDir); err != nil {
  137. return fmt.Errorf("unable to copy root directory to /app: %v", err)
  138. }
  139. if !s.hasMain {
  140. if err := synthesizeMain(tw, s.appFiles); err != nil {
  141. return fmt.Errorf("unable to synthesize new main func: %v", err)
  142. }
  143. }
  144. if err := tw.Close(); err != nil {
  145. return fmt.Errorf("unable to close tar file %v: %v", tarFile, err)
  146. }
  147. return nil
  148. }
  149. // synthesizeMain generates a new main func and writes it to the tarball.
  150. func synthesizeMain(tw *tar.Writer, appFiles []string) error {
  151. appMap := make(map[string]bool)
  152. for _, f := range appFiles {
  153. appMap[f] = true
  154. }
  155. var f string
  156. for i := 0; i < 100; i++ {
  157. f = fmt.Sprintf("app_main%d.go", i)
  158. if !appMap[filepath.Join(*rootDir, f)] {
  159. break
  160. }
  161. }
  162. if appMap[filepath.Join(*rootDir, f)] {
  163. return fmt.Errorf("unable to find unique name for %v", f)
  164. }
  165. hdr := &tar.Header{
  166. Name: f,
  167. Mode: 0644,
  168. Size: int64(len(newMain)),
  169. }
  170. if err := tw.WriteHeader(hdr); err != nil {
  171. return fmt.Errorf("unable to write header for %v: %v", f, err)
  172. }
  173. if _, err := tw.Write([]byte(newMain)); err != nil {
  174. return fmt.Errorf("unable to write %v to tar file: %v", f, err)
  175. }
  176. return nil
  177. }
  178. // imports returns a map of all import directories (recursively) used by the app.
  179. // The return value maps full directory names to original import names.
  180. func imports(ctxt *build.Context, srcDir string, gopath []string) (map[string]string, error) {
  181. pkg, err := ctxt.ImportDir(srcDir, 0)
  182. if err != nil {
  183. return nil, fmt.Errorf("unable to analyze source: %v", err)
  184. }
  185. // Resolve all non-standard-library imports
  186. result := make(map[string]string)
  187. for _, v := range pkg.Imports {
  188. if !strings.Contains(v, ".") {
  189. continue
  190. }
  191. src, err := findInGopath(v, gopath)
  192. if err != nil {
  193. return nil, fmt.Errorf("unable to find import %v in gopath %v: %v", v, gopath, err)
  194. }
  195. result[src] = v
  196. im, err := imports(ctxt, src, gopath)
  197. if err != nil {
  198. return nil, fmt.Errorf("unable to parse package %v: %v", src, err)
  199. }
  200. for k, v := range im {
  201. result[k] = v
  202. }
  203. }
  204. return result, nil
  205. }
  206. // findInGopath searches the gopath for the named import directory.
  207. func findInGopath(dir string, gopath []string) (string, error) {
  208. for _, v := range gopath {
  209. dst := filepath.Join(v, "src", dir)
  210. if _, err := os.Stat(dst); err == nil {
  211. return dst, nil
  212. }
  213. }
  214. return "", fmt.Errorf("unable to find package %v in gopath %v", dir, gopath)
  215. }
  216. // copyTree copies srcDir to tar file dstDir, ignoring skipFiles.
  217. func copyTree(tw *tar.Writer, dstDir, srcDir string) error {
  218. entries, err := ioutil.ReadDir(srcDir)
  219. if err != nil {
  220. return fmt.Errorf("unable to read dir %v: %v", srcDir, err)
  221. }
  222. for _, entry := range entries {
  223. n := entry.Name()
  224. if skipFiles[n] {
  225. continue
  226. }
  227. s := filepath.Join(srcDir, n)
  228. d := filepath.Join(dstDir, n)
  229. if entry.IsDir() {
  230. if err := copyTree(tw, d, s); err != nil {
  231. return fmt.Errorf("unable to copy dir %v to %v: %v", s, d, err)
  232. }
  233. continue
  234. }
  235. if err := copyFile(tw, d, s); err != nil {
  236. return fmt.Errorf("unable to copy dir %v to %v: %v", s, d, err)
  237. }
  238. }
  239. return nil
  240. }
  241. // copyFile copies src to tar file dst.
  242. func copyFile(tw *tar.Writer, dst, src string) error {
  243. s, err := os.Open(src)
  244. if err != nil {
  245. return fmt.Errorf("unable to open %v: %v", src, err)
  246. }
  247. defer s.Close()
  248. fi, err := s.Stat()
  249. if err != nil {
  250. return fmt.Errorf("unable to stat %v: %v", src, err)
  251. }
  252. hdr, err := tar.FileInfoHeader(fi, dst)
  253. if err != nil {
  254. return fmt.Errorf("unable to create tar header for %v: %v", dst, err)
  255. }
  256. hdr.Name = dst
  257. if err := tw.WriteHeader(hdr); err != nil {
  258. return fmt.Errorf("unable to write header for %v: %v", dst, err)
  259. }
  260. _, err = io.Copy(tw, s)
  261. if err != nil {
  262. return fmt.Errorf("unable to copy %v to %v: %v", src, dst, err)
  263. }
  264. return nil
  265. }
  266. // checkMain verifies that there is a single "main" function.
  267. // It also returns a list of all Go source files in the app.
  268. func checkMain(ctxt *build.Context) (bool, []string, error) {
  269. pkg, err := ctxt.ImportDir(*rootDir, 0)
  270. if err != nil {
  271. return false, nil, fmt.Errorf("unable to analyze source: %v", err)
  272. }
  273. if !pkg.IsCommand() {
  274. errorf("Your app's package needs to be changed from %q to \"main\".\n", pkg.Name)
  275. }
  276. // Search for a "func main"
  277. var hasMain bool
  278. var appFiles []string
  279. for _, f := range pkg.GoFiles {
  280. n := filepath.Join(*rootDir, f)
  281. appFiles = append(appFiles, n)
  282. if hasMain, err = readFile(n); err != nil {
  283. return false, nil, fmt.Errorf("error parsing %q: %v", n, err)
  284. }
  285. }
  286. return hasMain, appFiles, nil
  287. }
  288. // isMain returns whether the given function declaration is a main function.
  289. // Such a function must be called "main", not have a receiver, and have no arguments or return types.
  290. func isMain(f *ast.FuncDecl) bool {
  291. ft := f.Type
  292. return f.Name.Name == "main" && f.Recv == nil && ft.Params.NumFields() == 0 && ft.Results.NumFields() == 0
  293. }
  294. // readFile reads and parses the Go source code file and returns whether it has a main function.
  295. func readFile(filename string) (hasMain bool, err error) {
  296. var src []byte
  297. src, err = ioutil.ReadFile(filename)
  298. if err != nil {
  299. return
  300. }
  301. fset := token.NewFileSet()
  302. file, err := parser.ParseFile(fset, filename, src, 0)
  303. for _, decl := range file.Decls {
  304. funcDecl, ok := decl.(*ast.FuncDecl)
  305. if !ok {
  306. continue
  307. }
  308. if !isMain(funcDecl) {
  309. continue
  310. }
  311. hasMain = true
  312. break
  313. }
  314. return
  315. }