go-app-builderのソースコードを読む #golang #gae

## はじめに

みなさんはGAE/GoのSDKがどうやってコードをビルドしているのか知っていますか？
私は正直ふわっとしか理解できておらず、きっと小人さんがビルドしてるんだろうなくらいに思ってました。
当然、小人さんじゃなくて、どこかに`goapp build`とかを呼んでいるコードがあるんだろうとは思っていましたが、どうやら`go-app-builder`というツールがそれをやっていることを知ったので、調べてみることにしました。
今回は、ソースコードを読んでいく過程で分かったことをまとめたいと思います。

なお、ここではSDKのバージョンは`appengine-1.9.48`を使っています。

## ソースコードはどこにあるのか

さて、`go-app-builder`はどこにあるんでしょうか。きっとSDKのディレクトリのどこかです。
実はSDK以下にある`goroot`の中に入っています。

```
$ ls $(goapp env GOROOT)/src/cmd/go-app-builder
flags.go       gab.go         parser.go      synthesizer.go
```

ちなみに、`goapp env GOROOT`は`goapp`で使われている`GOROOT`を取得するコマンドです。`goapp env GOPATH`とすると`GOPATH`が返ってきます。OSによらず同じように実行できるので、ハンズオンとかで`GOPATH`確認するのに便利です。

さて、少し脱線しましたが、上記のコードを見ていきます。

## ソースコードを読む

まずは`main`関数があるファイルから読むのが常套手段ですね。
`main`関数があるのは、`gab.go`ですね。

`main`関数を読んでいくと、193行目に以下のような行があります。

```
193     err = buildApp(app)
```

なるほど、`buildApp`できっとビルドしてるんでしょうね。

それにしても、引数で渡している`app`ってなんでしょうね。
`buildApp`を読む前に、こちらを先に調べましょう。

`app`は、`gab.go`の166行目で作られた変数です。

```
166     app, err := ParseFiles(baseDir, flag.Args(), ignoreReleaseTags)
```

どうやら、ソースコードをパースしているようです。
この関数を探しましょう。
`parser.go`という名前のファイルがあるので、あやしいですね。

やはり、`parser.go`に`ParseFiles`がありました。

```
129 func ParseFiles(baseDir string, filenames []string, ignoreReleaseTags bool) (*App, error) {
```

では、`ParseFiles`関数を読んでいきます。
まずは、戻り値の`App`が何者か知らないといけません。
`parser.go`の28-36行目に定義を見つけました。

```
 28 // App represents an entire Go App Engine app.
 29 type App struct {
 30     Files        []*File    // the complete set of source files for this app
 31     Packages     []*Package // the packages
 32     RootPackages []*Package // the subset of packages with init functions
 33     HasMain      bool       // whether the app has a user-defined main package
 34
 35     PackageIndex map[string]*Package // index from import path to package object
 36 }
```

なるほど、アプリケーションを構成するファイルやパッケージの情報を持つみたいですね。
`HasMain`が気になりますね。

`App.HasMain`を代入しているところを探してみましょう。

```
186             // Skip any main.main that isn't at the application root.
187             if file.HasMain && dir == "." {
188                 app.HasMain = true
189             }
```

186-189行目で代入していますね。
`file.HasMain`という値が`true`だったら入れてるっぽいですね。
`file`はきっと上記の`App`構造体の`Files`の要素でしょうね。
ついでなので、`File`型の定義を見てみましょう。

```
65 type File struct {
66     Name        string   // the file name
67     PackageName string   // the package this file declares itself to be
68     ImportPaths []string // import paths
69     HasInit     bool     // whether the file has an init function
70     HasMain     bool     // whether the file defines a main.main function
71     callsAEMain bool     // whether the main function, if it exists, calls appengine.Main
72 }
```

なるほど、`init`関数を持っているのか、`main`関数を持っているのかという情報を持っているようですね。
おや？`callsAEMain`というフィールドがありますね。コメントを読む限り、`appengine.Main`というものを呼んでいるかどうかのようです。

`appengine.Main`とは何でしょう？[ドキュメント](https://godoc.org/google.golang.org/appengine#Main)を読んでみます。

```
On App Engine Standard it ensures the server has started and is prepared to receive requests.
```

なるほど、`main`関数の中で呼び出すことで、`init`関数で使わずにGAEのエントリーポイントを実行できるんですね。
`init`関数があったら、`HasInit`が`true`になり、`main`関数があったら`HasMain`が`true`になるわけですね。
そして、`callsAEMain`が`true`の場合は`main`関数で`appengine.Main`を呼んでいるというわけですね。

`appengine.Main`を呼んでいるかどうかは、どの辺でチェックしているんでしょうね。
`parser.go`の中を探します。

どうやら、`parseFile`関数の中でやっているようです。

```
488             if file.Name.Name == "main" && isMain(funcDecl) {
489                 hasMain = true
490                 callsAEMain = callsAppEngineMain(fset, funcDecl)
491             }
```

おぉ。`funcDecl`とか出てきましたね。`go/ast`パッケージで定義されている、[`*ast.FuncDecl`](https://golang.org/pkg/go/ast/#FuncDecl)のようです。

`ast.FuncDecl`は関数定義を表した型で、以下のように定義されています。

```
type FuncDecl struct {
        Doc  *CommentGroup // associated documentation; or nil
        Recv *FieldList    // receiver (methods); or nil (functions)
        Name *Ident        // function/method name
        Type *FuncType     // function signature: parameters, results, and position of "func" keyword
        Body *BlockStmt    // function body; or nil (forward declaration)
}
```

きっと、`Ident`フィールドとかに、関数名の情報が入ってて、そこから`main`関数だったりの情報を取得しているわけですね。
そして、`main`関数を見つけたら、中身を見て`appengine.Main`を呼んでいるか調べているのでしょう。

`callsAppEngineMain`関数を見てやれば、`appengine.Main`を呼んでいるかどうかを調べている処理が見つかりそうです。

```
412 // callsAppEngineMain returns whether or not the given function calls
413 // appengine.Main(). This is required in user-provided main() functions.
414 func callsAppEngineMain(fset *token.FileSet, f *ast.FuncDecl) bool {
415     for _, expr := range f.Body.List {
416         expStmt, ok := expr.(*ast.ExprStmt)
417         if !ok {
418             continue
419         }
420         callExpr, ok := expStmt.X.(*ast.CallExpr)
421         if !ok {
422             continue
423         }
424         selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
425         if !ok {
426             continue
427         }
428         selX, ok := selExpr.X.(*ast.Ident)
429         if !ok {
430             continue
431         }
432         if selX.Name == "appengine" && selExpr.Sel.Name == "Main" {
433             return true
434         }
435     }
436     return false
437 }
```

なるほど。`ast.FuncDecl`構造体の`Body`フィールドから関数の本体を表す[ast.BlockStmt](https://golang.org/pkg/go/ast/#BlockStmt)が取れ、その`List`フィールドから文を表す[`ast.Stmt`](https://golang.org/pkg/go/ast/#Stmt)のスライスが取得できるようです。

`ast.BlockStmt`の定義は以下のようなっています。

```
type BlockStmt struct {
        Lbrace token.Pos // position of "{"
        List   []Stmt
        Rbrace token.Pos // position of "}"
}
```

上記の`ast.Stmt`を一つずつ見ていき、関数呼び出しを見つけて、その関数呼び出しが`appengine.Main`を呼び出していればOKという感じでしょうか。
インポートパスを見ておらず、`appengine`というパッケージ名と`Main`という関数名しか見ていないのは、`goapp env GOROOT`にある`appengine`パッケージか`google.golang.org/appengine`なのか区別を付けないためにあるんでしょうか。
適当に`appengine`パッケージを作ったらどうなるのかなぁという衝動を抑えつつ、コードリーディングに戻りましょう。

さて、`init`関数を使った場合、どこかで`appengine.Main`を呼ぶコードを足しているはずですね。
その部分を探してみましょう。

`synthesizer.go`というファイルに、`MakeMain`という怪しい関数を見つけました。
きっとここで`main`関数を作ってるに違いありません。

```
15     if err := mainTemplate.Execute(buf, app); err != nil {
```

おぉ！テンプレートに埋め込んでますね！これは`go test`でも取ってる手段ですね。
テンプレートを見てみましょう。

```
37 var mainTemplate = template.Must(template.New("main").Parse(
38     `package main
39
40 import (
41     internal "appengine_internal"
42
43     // Top-level app packages
44     {{range .RootPackages}}
45     _ "{{.ImportPath}}"
46     {{end}}
47 )
48
49 func main() {
50     internal.Main()
51 }
52 `))
```

おや？`appengine.Main`じゃなくて、`internal.Main`を呼んでますね。
何が違うんでしょうか？`appengine.Main`の[実装](https://github.com/golang/appengine/blob/master/appengine.go#L53-L55)を見てましょう。

```
func Main() {
	internal.Main()
}
```

(´・ω・｀)同じでした。

予想通り、`init`関数の場合は、テンプレートを使って`main.main`関数を作成し、それをエントリーポイントにしているようです。

さぁ、次はビルドしているところを探しましょう。
`gab.go`に戻ります。

ビルドをしているところは、きっと外部コマンドを実行する必要があるので、[`os/exec.Cmd`](https://golang.org/pkg/os/exec/#Cmd)を使っているところを探してみましょう。

`parser.go`に、`run`という関数を見つけました。

```
676 func run(args []string, env []string) error {
677     if *verbose {
678         log.Printf("run %v", args)
679     }
680     tool := filepath.Base(args[0])
681     if *trampoline != "" {
682         // Add trampoline binary, its flags, and -- to the start.
683         newArgs := []string{*trampoline}
684         if *trampolineFlags != "" {
685             newArgs = append(newArgs, strings.Split(*trampolineFlags, ",")...)
686         }
687         newArgs = append(newArgs, "--")
688         args = append(newArgs, args...)
689     }
690     cmd := &exec.Cmd{
691         Path:   args[0],
692         Args:   args,
693         Env:    env,
694         Stdout: os.Stdout,
695         Stderr: os.Stderr,
696     }
697     if err := cmd.Run(); err != nil {
698         return fmt.Errorf("failed running %v: %v", tool, err)
699     }
700     return nil
701 }
```

今度は`run`関数を呼んでいる部分を探してみましょう。

```
543 func (t *timer) run(args, env []string) error {
544     start := time.Now()
545     err := run(args, env)
546
547     t.mu.Lock()
548     t.n++
549     t.total += time.Since(start)
550     t.mu.Unlock()
551
552     return err
553 }
```

`timer`という型のメソッドで呼ばれているようです。
なるほど、次はこの`timer`型を使っている場所を探しましょう。

```
204 // Timers that are manipulated in buildApp.
205 var gTimer, lTimer, sTimer timer // manipulated in buildApp
```

なるほど！3つあるようですね。`timer`型の定義をみると、`name`というフィールドがあるようですね。

```
535 type timer struct {
536     name string
537
538     mu    sync.Mutex
539     n     int
540     total time.Duration
541 }
```

`name`フィールドを初期化している場所を探してみると、以下のようなコードを見つけました。

```
189     gTimer.name = "compile"
190     lTimer.name = "link"
191     sTimer.name = "skip"
```

ほうほう。なるほど、`compile`と`link`とか怪しいですね。
`gTimer`と`lTimer`の行方を追ってみましょう。

```
505     // Run the actual compilation.
506     if err := gTimer.run(args, c.env); err != nil {
507         return err
508     }
```

どうやら、`gTimer.run`は`compiler`という型の`compile`メソッドで呼ばれています。
`compiler`型の初期化を行っているところ探してみましょう。

```
270     // Compile phase.
271     c := &compiler{
272         app:              app,
273         goRootSearchPath: goRootSearchPath,
274         compiler:         toolPath("compile"),
275         env:              env,
276     }
```

なるほど、`toolPath`関数が怪しいですね。

```
655 func toolPath(x string) string {
656     ext := ""
657     if runtime.GOOS == "windows" {
658         ext = ".exe"
659     }
660     return filepath.Join(*goRoot, "pkg", "tool", runtime.GOOS+"_"+fullArch(*arch), x+ext)
661 }
```

なるほど、`$(goapp env GOROOT)/pkg/tool/`以下にOSとアーキテクチャにあったツール類があるようです。
手元のOSXで何があるのか見てみましょう。

```
ls $(goapp env GOROOT)/pkg/tool/darwin_amd64
asm     cgo     compile cover   link
```

おぉ。これは`go tool compile`や`go tool link`で呼び出されるものと同じですね。
ちなみに、`go build`を呼ばすに`go tool compile`と`go tool link`でビルドするには以下のような手順で実行します。
`go tool compile`でコンパイルし、`go tool link`でリンクしています。

```
$ go tool compile -o main.a main.go
$ go tool link -o main main.a
```

さて、`compile`の引数に`-u`というオプションを渡しているところがありました。

```
346     if !*unsafe {
347         // reject unsafe code
348         args = append(args, "-u")
349     }
```

どうやらここで、`unsafe`なパッケージをリジェクトしているようです。
`syscall`はどこでリジェクトしてるんでしょうか？
`syscall`で探してみます。

`parser.go`で見つけました。

```
514 // checkImport will return whether the provided import path is good.
515 func checkImport(path string) bool {
516     if path == "" {
517         return false
518     }
519     if len(path) > 1024 {
520         return false
521     }
522     if filepath.IsAbs(path) || strings.Contains(path, "..") {
523         return false
524     }
525     if !legalImportPath.MatchString(path) {
526         return false
527     }
528     if path == "syscall" || path == "unsafe" {
529         return false
530     }
531     return true
532 }
```

どうやら各ファイルのインポートパスを調べて、`syscall`や`unsafe`を見つけるとリジェクトするようです。
ついでに、相対パスでインポートするのもリジェクトしているようですね。

`legalImportPath`にマッチしないものもリジェクトしているようです。
呼び出しているメソッドを見る限り正規表現のようなので、定義を見てみましょう。

```
512 var legalImportPath = regexp.MustCompile(`^[a-zA-Z0-9_\-./~+]+$`)
```

なるほど？なんかおかしい気がする。
この正規表現は`~`や`.`を[許してる](https://play.golang.org/p/TDpX5xKDWc)ようです。。。
識別子には`~`や`.`は使えません。日本語とかは使えるんですが。
謎のチェックですね。。

## まとめ

さて、まとめるといいつつ、ダラダラとコードリーディングしながら書きなぐりましたが、コードリーディングしてみて分かったことを以下にざっとまとめたいと思います。

* `appengine.Main`を呼び出せば、`main`関数は使えるようだ
* `unsafe`と`syscall`以外も、相対パスなインポートパスもNG
* `go compile -u`で`unsafe`の呼び出しをリジェクトできる
* `go`パッケージを知ってると世界が広がる

ソースコードみると、以外に知らないことがあったりするので、せっかくなので読んでみるといいですね。