nginx-build internals

この記事は、[nginx Advent Calendar 2015](http://qiita.com/advent-calendar/2015/nginx)の8日目の記事です。

# nginx-build

[nginx-build](https://github.com/cubicdaiya/nginx-build)はnginxをビルドするためのコマンドラインツールです。

使い方については過去にQiitaに書いた記事やnginx-buildのREADME.mdにそれなりに詳しく書いてあるのでそちらを参照して下さい。

 * [cubicdaiya/nginx-build/README.md](https://github.com/cubicdaiya/nginx-build/blob/master/README.md)
 * [nginx-buildでnginxをビルドする](http://qiita.com/cubicdaiya/items/6b7dd441eaa8a0c5f75d)
 * [nginx-buildでnginxにサードパーティモジュールを組み込む](http://qiita.com/cubicdaiya/items/b50bc10c91f5b3a3ec4c)
 * [nginx-buildでnginxの依存ライブラリを静的に組み込む](http://qiita.com/cubicdaiya/items/51824efba50906b30970)

今回はnginx-buildの内部実装について少し解説します。わりと泥臭いこともやってます。

# nginx-build internals

nginx-buildが行っているのは大きく分けて以下の3つです。

 * nginxや依存ライブラリ、拡張モジュールのソースコードのダウンロード
 * configureのオプション文字列を自動生成
 * makeを実行

nginx-buildはGoで書かれていますが、中でもGoの特徴を最も活かしているのが最初のダウンロード処理部分です。

## ゴルーチンを利用した並行ダウンロード

nginx-buildでは指定したオプションにもよりますが、実行すると複数のソースコードをダウンロードしてワーキングディレクトリに展開します。

 * nginx
 * PCRE
 * OpenSSL
 * 拡張モジュール群

各々のダウンロード処理は`wget`、`git`、`hg`のいずれかによって行われます。実際の処理はこんな感じです。(ちょっと前までカウンターとチャネル使って実装してたけど`sync.WaitGroup`使って書き直した)

```go:nginx-build.go
        wg := new(sync.WaitGroup)
        if *pcreStatic {
                wg.Add(1)
                go downloadAndExtractParallel(&pcreBuilder, wg)
        }
 
        if *openSSLStatic {
                wg.Add(1)
                go downloadAndExtractParallel(&openSSLBuilder, wg)
        }
 
        if *zlibStatic {
                wg.Add(1)
                go downloadAndExtractParallel(&zlibBuilder, wg)
        }
 
        wg.Add(1)
        go downloadAndExtractParallel(&nginxBuilder, wg)
 
        if len(modules3rd) > 0 {
                wg.Add(len(modules3rd))
                for _, m := range modules3rd {
                        go downloadAndExtractModule3rdParallel(m, wg)
                }
 
        }
 
        // wait until all downloading processes by goroutine finish                                                                                           
        wg.Wait()
```

シェルスクリプトだとこの手の処理をうまくやるのはしんどいですが、Goだととても楽です。

## ビルドエラーになりやすいポイントを自動的にカバー

nginxに限った話ではありませんが、プログラムをソースコードからビルドしようとするとハマリどころと言いますか、ビルドの実行環境やconfigureオプション、依存ライブラリなどの外的要因によってプログラムがうまくビルドできないことがあります。

nginxでもそういうのがいくつかあって、あまりにもハマリやすそうなものについてはnginx-build側でカバーするようにしています。(大体MacかOpenSSLで起きる)

## OpenSSLとngx_http_ssl_module

例えば`--with-openssl`オプションを利用してOpenSSLを静的リンクしてnginxをビルドする場合を考えてみましょう。

```
./configure --with-openssl={OpenSSLのソースコードディレクトリへのパス}
```

実は上記のやり方だとnginxはOpenSSLを静的リンクしません。というかオプション自体を無視します。(でも`nginx -V`の`configure arguments`の出力にはでるからハマる)

OpenSSLを静的リンクするには`--with-http_ssl_module`も合わせて必要になります。そもそもOpenSSLはこのモジュールのためにあるようなものなのである意味当然と言えるかもしれません。

```
./configure --with-openssl={OpenSSLのソースコードディレクトリへのパス} --with-http_ssl_module
```

nginx-buildであれば以下のように実行するだけでOpenSSLを静的リンクしてnginxをビルドできます。

```
nginx-build -d work -openssl
```

nginx-buildでは上記のようにOpenSSLは静的リンクするんだけども`--with-http_ssl_module`が指定されていない場合は`--with-http_ssl_module`を自動的に付加する処理が入っています。

```go:configure.go
        if openSSLStatic && !strings.Contains(configure, "--with-http_ssl_module") {
                configure += "--with-http_ssl_module \\\n"
        }
```

### OpenSSLの静的リンクと並列ビルド

nginx-buildはnginxをビルドする際にmakeを実行します。また、コマンドラインオプション(`-j`)でジョブ数を指定することができてデフォルトはCPUのコア数です。

```go:nginx-build.go
        jobs := flag.Int("j", runtime.NumCPU(), "jobs to build nginx")
```

このジョブ数はnginxだけでなくPCREやzlib、OpenSSLといった外部ライブラリのコンパイルにも適用されます。もちろん静的リンクする場合のみの話です。そしてOpenSSLは**makeのジョブ数を1より大きい値にするとコケます**。これはログをパッと見ただけでは原因がわかりづらいので一時期相当ハマりました。

なので今はこんな感じのコードが入っています。

```go:nginx-build.go
        if *openSSLStatic {
                // Workarounds for protecting a failure of building nginx with static-linked OpenSSL.                                                         
 
                // Unfortunately a build of OpenSSL fails when multi-CPUs are used.                                                                           
                *jobs = 1
```

OpenSSLのコンパイルはとても時間がかかるのでこれなんとかしたい。

### Mac上でOpenSSLを静的リンク

またOpenSSLかよ、と思ったあなた、気が合いますね。私も正直ちょっとうんざりしています。でも、もうちょっとだけ続くんじゃよ。


さっきも書きましたが、nginxでOpenSSLを静的リンクするにはconfigureに以下のオプションが必要になります。

```
./configure --with-openssl={OpenSSLのソースコードディレクトリへのパス} --with-http_ssl_module
```

しかし、これだけだと以下の警告メッセージが出てしまいます。(そしてビルドは止まります)

```
WARNING! If you wish to build 64-bit library, then you have to
         invoke './Configure darwin64-x86_64-cc' *manually*.
         You have about 5 seconds to press Ctrl-C to abort.
```

Macはあまり詳しくないのですが、どうも自分のMacだと`uname -m`の出力は`x86_64`なのに`uname -p`の出力が`i386`なっているせいで64bit環境用のライブラリとしてビルドできないのでこのエラーが出るようです。このエラーは環境変数で`KERNEL_BITS=64`ってやれば回避できます。

```go:nginx-build.go
                // Sometimes machine hardware name('uname -m') is different                                                                                   
                // from machine processor architecture name('uname -p') on Mac.                                                                               
                // Specifically, `uname -p` is 'i386' and `uname -m` is 'x86_64'.                                                                             
                // In this case, a build of OpenSSL fails.                                                                                                    
                // So it needs to convince OpenSSL with KERNEL_BITS.                                                                                          
                if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" {
                        os.Setenv("KERNEL_BITS", "64")
                }
```