目次へ戻る §28へ戻る

29. MinGW gcc による Go のソースからの構築

2013.11.20 (鈴)

29.1 はじめに

Four years of Go - The Go Blog [golang.org] で祝っているように,今月,2013年 11月に Go 言語がオープン・ソース・プロジェクトとして4周年を迎えました:

10 November 2013

Today marks the fourth anniversary of Go as an open source project.

そして筆者が Go 言語についてはじめて書いたのは1年前の 11月 でした。列挙すると下記のとおりです。

  1. 2012-11-30 Go 言語による LINQ to Objetcs の試作
  2. 2012-12-14 C と Go 言語 (Mac OS X)
  3. 2013-01-23 有理数を含む混合演算のための Go 言語パッケージの試作
  4. 2013-02-15 Go 言語による有理数電卓
  5. 2013-04-04 Go 言語による簡単な Lisp
  6. 2013-04-19 ゴルーチンによる Lisp Future 形式の実現
1. では C# の同名のライブラリに似た機能の実現のために,関数の型そのものに対してメソッドを新しく定義できるという (手続き型言語としては) 類例のない Go の特徴を使いました。 2. では C 言語と相互に呼び出す方法を確かめ OpenGL プログラムに応用しました。 3. から 5. では巨大整数や字句解析など特色ある品ぞろえの標準ライブラリ,単体テストと統合されたドキュメント生成などの機能を試しました。 6. ではゴルーチンとチャネルによるコンカレント演算の効果を 5. をもとに確かめました。

この記念すべき 11 月にちなんで今回は §27.6 で取り上げた 64 ビット Cygwin 版 MinGW gcc で Go をソースから構築してみます。 使う Cygwin パッケージは

です。また,Go のソースとして, Getting Started - The Go Programming Language [golang.org] にあるリンクをたどった Downloads - go [google.com] から執筆時現在最新の go1.2rc5.src.tar.gz を入手して使います。

go1.2rc4 を使って本稿の内容が一応できあがったのが 11月18日の昼,しかしその約1時間前に go1.2rc5 がリリースされていました。 できた瞬間から時代遅れというのもなんですから go1.2rc5 に対応するため本稿の完成を延ばしました。 このときコードや手順に修正の必要はありませんでした。 まもなく go1.2 正式版がリリースされると思いますが,やはり本稿の内容に修正は必要ないはずです。

29.2 gcc, g++, ar のラッパ

Go のソースからの構築手順は Installing Go from source - The Go Programming Language [golang.org] に説明されています。要約すれば次の2行です。

$ cd go/src
$ ./all.bash

Windows の場合は ./all.bash ではなく all.bat を使います。

問題は Windows ではコマンド プロンプトからバッチ・ファイル経由で MinGW gcc コンパイラを起動することを前提としていることです。 Cygwin 版ではない本来の MinGW | Minimalist GNU for Windows [mingw.org] の gcc が必要です。

しかし,せっかく Cygwin にも同じコンパイラがあるのですから,ここではそれを使うことを考えます。 Cygwin 版の MinGW gcc は cygwin1.dll に依存した Cygwin プログラムですが,§27.6 で説明したようにそれでコンパイルして作成したプログラムは Cygwin から独立した真正の Windows プログラムになります。

一つの自明な方法は C:\cygwin64\binPATH を通すことにより,バッチ・ファイルから直接 Cygwin 版 MinGW gcc を起動できるようにすることです。 しかし,この方法は簡単な場合にはうまくいきますが,コマンド行引数の渡し方の問題のため必ずしも正しく動作しません。 Cygwin 版 MinGW gcc 起動用バッチ・ファイルを作った場合も同様の問題があります。 コマンド引数上の2重引用符が取り去られ,DOS 形式のパス名に含まれる \ によって字句解析エラーに終わります。

そこで下記のラッパ (wrapper) プログラムを作りました。 これを gcc.exe としてバッチ・ファイル等から呼び出すと, C:\cygwin64\binPATH を通し,コマンド行引数を調整して x86_64-w64-mingw32-gcc.exe を呼び出し,その終了ステータスを自分の終了ステータスとします。 Go の構築では gcc.exe のほかに g++.exear.exe も使われますから,そのラッパとしても動作するようにしました。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>

void printErrorAndExit()
{
    DWORD err = GetLastError();
    fprintf(stderr, "Error: %u\n", (unsigned)err);
    exit(1);
}

void addPath(const char* path)
{
    DWORD len = GetEnvironmentVariableA("PATH", NULL, 0);
    DWORD path_len = strlen(path);
    char buf[path_len + 1 + len];
    strcpy(buf, path);          // pathlen
    strcat(buf, ";");           // + 1
    GetEnvironmentVariableA("PATH", buf + path_len + 1, len);
    // printf("len=%d, buf='%s'\n", len, buf);
    if (SetEnvironmentVariableA("PATH", buf) == 0)
        printErrorAndExit();
}

int addArg(char* buf, int offset, const char* str)
{
    int len = 0;
    int ch;
    for (;;) {
        ch = *str++;
        if (ch == '\\') {
            len++;
            if (buf != NULL)
                buf[offset++] = ch;
        }
        if (buf != NULL)
            buf[offset++] = ch;
        if (ch == '\0')
            break;
        len++;
    }
    return len;                 // 末尾の \0 を含まないバイト数
}

int makeCommandLine(char* buf, const char* cmd, int argc, const char** argv)
{
    int len = strlen(cmd);
    if (buf != NULL) 
        strcpy(buf, cmd);
    int i;
    for (i = 1; i < argc; i++) {
        len += addArg(buf, len, " '");
        len += addArg(buf, len, argv[i]);
        len += addArg(buf, len, "'");
    }
    return len + 1;             // 末尾の \0 を含むバイト数
}


int main(int argc, const char** argv)
{
    addPath("C:\\cygwin64\\bin");
    const char* cmd;
    if (strstr(argv[0], "g++") != NULL) {
        cmd = "x86_64-w64-mingw32-g++.exe";
    } else if (strstr(argv[0], "gcc") != NULL) {
        cmd = "x86_64-w64-mingw32-gcc.exe";
    } else if (strstr(argv[0], "ar") != NULL) {
        cmd ="C:\\cygwin64\\usr\\x86_64-w64-mingw32\\bin\\ar.exe";
    } else {
        fprintf(stderr, "What: %s\n", argv[0]);
        return 1;
    }
    int len = makeCommandLine(NULL, cmd, argc, argv);
    char buf[len];
    makeCommandLine(buf, cmd, argc, argv);
    //fprintf(stderr, "\nbuf=%s\n", buf);

    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    GetStartupInfo(&si);
    if (CreateProcessA(NULL, buf,
                       NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi) == 0)
        printErrorAndExit();
    DWORD w = WaitForSingleObject(pi.hProcess, INFINITE);
    if (w == WAIT_FAILED) {
        printErrorAndExit();
    } else if (w != WAIT_OBJECT_0) {
        fprintf(stderr, "Wait Result: %u\n", (unsigned)w);
        return 1;
    }
    DWORD status;
    if (GetExitCodeProcess(pi.hProcess, &status) == 0)
        printErrorAndExit();

    //fprintf(stderr, "status=%u\n\n", (unsigned)status);
    return status;
}

このプログラムは起動されるとまず addPath()C:\cygwin64\binPATH の先頭に追加します。

ワイド文字列ではなく char 文字列を使っていますから GetEnvironmentVariableA(), SetEnvironmentVariableA() のようにいわゆる A 系 API を使います。 A 系 API は locale によって扱えない文字 (日本の場合は SJIS で表現できない Unicode 文字) がありますから今の時代に好ましくありませんが,今回の目的にはさしあたり問題ありません。

次に argv[0] を調べてどのコマンドを実行するかを決めます。

それから makeCommandLine()argv[1] 以降の引数を次のように変換してコピーします。

動的なメモリ確保のためにヒープ・メモリを使ってもよいのですが,せっかく gcc なのですから C99 で標準規格にもなった可変長配列を使います。下記の2箇所です。 スタックあふれの可能性の増大と引き換えに,時間的にも空間的にも良い効率が期待できます。

Win32 API の GetEnvironmentVariable() は可変長配列と相性が良い設計になっています。 ある意味,gcc などと組み合わせてようやく本来の姿で使えます。 makeCommandLine() も GetEnvironmentVariable() の流儀に準じて二回呼びで可変長配列が使えるようにしました。 addPath() では可変長配列を SetEnvironmentVariable() に値のバッファとして渡していますが,同関数は Unix の setenv() と同じくバッファのコピーをとりますから,addPath() から戻って領域が消えても問題ありません。

buf に組み立てたコマンド行を使って CreateProcess() を呼び出します。 ここでは第5引数を TRUE にすることが重要です。 そうして親プロセスからハンドルを継承するようにしないと,入力ファイルを - (標準入力) とする一部の箇所で gcc が正常に動作できません。

実際,はじめうちは第5引数を FALSE にしていたせいで Go の構築の途中でエラー終了していました。

プロセスの終了を WaitForSingleObject() で待ち合わせ,GetExitCodeProcess() で終了ステータスを status 変数に得ます。 status 変数の値を自分自身の終了ステータスとしてプログラムを終えます。

ラッパ・プログラムにとって子プロセスの終了を待ち合わせて終了ステータスを継承することは大変に重要です。 もし待たなかったら,子プロセスの gcc でコンパイルが終わっていないのに,それに依存する次のコンパイルを始めてしまいます。 終了ステータスを継承しなかったら,コンパイルに失敗しても,それに依存する次のコンパイルを始めてしまいます。

このラッパ・プログラムを gcc.exe, g++.exe, ar.exe として用意し PATH 上に置けば,Cygwin とは別にわざわざ MinGW のコンパイラ一式をインストールしなくても Go をソースから構築できるわけです。

29.3 構築作業

さしあたり分かりやすいようにデスクトップ上に work フォルダを設けてそこで構築することにします。 前節のラッパ・プログラムのソースを gcc.c として用意します。 下記のようなバッチ・ファイル setpath.bat を用意します。

@echo off
set CYGWIN=nodosfilewarning
PATH %USERPROFILE%\Desktop\work;%PATH%

環境変数 CYGWINnodosfilewarning とするのはいわゆる DOS 形式のファイル名の使用に対し Cygwin が警告しないようにするためです。 PATH の先頭に %USERPROFILE%\Desktop\work を追加してデスクトップ上の work フォルダに PATH を通します。 ここに gcc.exe, g++.exe, ar.exe という名前でラッパ・プログラムを置きます。

gcc.csetpath.batgcc-wrap-x64-1.tar.bz2 にアーカイブしてあるとします (本ページ冒頭からダウンロードできます)。 ~/Desktop がデスクトップ・フォルダ (この例では C:\Users\suzuki\Desktop) へのシンボリック・リンクとして用意されているとします。 下記のように work フォルダをデスクトップに準備します。 (実際の様子: )。

01:~$ cd Desktop
01:~/Desktop$ mkdir work
01:~/Desktop$ cd work/
01:~/Desktop/work$ tar xf ~/Downloads/go1.2rc5.src.tar.gz
01:~/Desktop/work$ ls
go/
01:~/Desktop/work$ tar xf ~/Downloads/gcc-wrap-x64-1.tar.bz2
01:~/Desktop/work$ ls
gcc.c*  go/  setpath.bat*
01:~/Desktop/work$ x86_64-w64-mingw32-gcc -Wall gcc.c -o gcc
01:~/Desktop/work$ cp -a gcc.exe g++.exe
01:~/Desktop/work$ cp -a gcc.exe ar.exe
01:~/Desktop/work$ ls -l
total 417
-rwxr-xr-x  1 suzuki None 137288 Nov 19 08:23 ar.exe*
-rwxr-xr-x  1 suzuki None 137288 Nov 19 08:23 g++.exe*
-rwxr-xr-x  1 suzuki None   2734 Nov 18 11:03 gcc.c*
-rwxr-xr-x  1 suzuki None 137288 Nov 19 08:23 gcc.exe*
drwxr-xr-x+ 1 suzuki None      0 Nov 19 08:21 go/
-rwxr-xr-x  1 suzuki None     80 Nov 18 11:03 setpath.bat*
01:~/Desktop/work$  

コマンド プロンプトを起動します。 work フォルダの setpath.bat で環境変数を設定してから go 言語のソース・ツリーで構築を開始します。 (実際の様子: )

C:\Users\suzuki>cd Desktop

C:\Users\suzuki\Desktop>cd work

C:\Users\suzuki\Desktop\work>setpath.bat

C:\Users\suzuki\Desktop\work>cd go

C:\Users\suzuki\Desktop\work\go>cd src

C:\Users\suzuki\Desktop\work\go\src>all.bat

次のように表示されて構築が進行します。

# Building C bootstrap tool.
cmd/dist

# Building compilers and Go bootstrap tool.
lib9
libbio
libmach
misc/pprof
cmd/addr2line
cmd/nm
cmd/objdump
……以下略……

構築が進行し「# Testing packages.」の段階に入ってしばらくすると 「このプログラムの機能のいくつかが Windows ファイアウォールでブロックされています」 と題されたダイアログが出るかもしれません。 このときは 「キャンセル」 を選択します。キャンセルしてもテストは失敗しません。


このとき,もしも「アクセスを許可する(A)」を選択すると,許可対象である net.test が一時フォルダ込みのパス名で登録されますから,二度と使われないファイアウォール規則が無駄に増えることになります。

マシンにもよりますが,開始から数分または十数分後には「ALL TESTS PASSED」と表示されて無事終了するはずです。

構築された結果は Cygwin に依存しません。 gobin フォルダに PATH を通せば普通に 64 ビット版の go コマンドが使えるようになります。

今しがた作った go コマンドにバージョンを表示させると go version go1.2rc5 windows/amd64 と表示されます。


この go コマンドで ゴルーチンによる Lisp Future 形式の実現 の Tiny Lisp のソースをコンパイルすると実行可能な tiny-lisp ができ上がります。 go コマンドも,それでコンパイルした tiny-lisp も Cygwin なしに動きます。

29.4 おわりに

ここでは作業場所へのアクセスのしやすさと片付けのしやすさからデスクトップ上で構築しました。 もし構築結果を恒久的に使うならば go フォルダを展開する場所についてより慎重に検討してください。

また,ここではラッパ・プログラムを Cygwin に依存しない真正の Windows プログラムとして書きました

コードのスタイルはあまり Windows の C 言語プログラムらしくありませんが……。

終わってから気づいたことですが,どのみち構築前に PATH にフォルダを加えるならば,別解として,Cygwin の C:\cygwin64\bin にも PATH を通し,ラッパ・プログラムを Unix の API を使って書いてもよかったかもしれません。 コードがより簡素になったはずです。 実体は Windows 上の動作ですから,効率としては Win32 API をじかに使う現在のラッパ・プログラムより劣ることが予想されますが,対応する Win32 API 版をたたき台として使えますから,Windows に慣れた人にとっては Cygwin (Unix) プログラミングの演習課題として手頃かもしれません。

両者の API を比べると,Unix が子プロセスの生成手順を fork() と exec() の2段階に分けたのは (少なくとも API 利用者の立場からは) 大変にエレガントな解だったことが分かります。 プロセス起動に関するさまざまなオプションを fork() 直後の子プロセス用の分岐の中でアプリケーション・コードとして実装できます。 Windows の CreateProcess() にはそのようなコードをはさむ余地がありませんから,API 仕様を複雑化して様々な需要に応えられるようにしなくてはなりません。 ラッパ・プログラムについていえばそうしたコードも不要で,ただ Unix の単純なプロセス生成の関数を使うという利点を享受できるはずです。




Copyright (c) 2013 OKI Software Co., Ltd.