最小構成でWindows版Rubyを動かす(Rubyのビルド手順付)

この記事は対応を考えながら記述しているので、ちゃんとまとまっていない。また、完全にはうまくいっていない。進展があったら、まとめなおそうと思う。
また、有用な情報があれば、提供願いたい。

# 問題

コアなRubyユーザは自分のPCにRubyをインストールして利用していると思う。
しかし、作ったツール類を他のWindowsユーザに利用してもらうための環境準備は非常に面倒。

# 解決策

解決策として、以下の3通りの方法があると思うが、ここにあげたうちの3番目の対応を考える。
(開発者の多少の手間は目をつぶるとして、利用者に苦労がないようにという配慮から)

## 方法
1. 利用者にRubyをインストールしてもらう
2. [Ocra](https://rubygems.org/gems/ocra)を使ってexe化したものを配布
3. ツールにruby.exeを同梱してフォルダ単位で配布

## 欠点
以下、それぞれの方法に対する欠点をまとめてみた。

1. バージョンを揃えることが開発者にも利用者にも難しい
2. exe実行時にアーカイブしたrubyをtmpに展開するとき、ちょっと遅い。スクリプト言語の開発にコンパイルフェーズが発生してなんか嫌。他人の環境に配ったツールに問題があった時に調査や暫定対処のための修正ができない。
3. 依存するgemやライブラリに絞って配布する手間が難しい。なお、rubyのトップフォルダ全部を配布物に含めると結構なサイズになるしコピーも時間がかかる

# 方法3のフォルダ構成

以下のフォルダ構成にしてツールを配布することを考える。rubyフォルダ以下のライブラリ類は必要最低限に絞る方針。

```
toolフォルダ\
  +-- ruby\   (ruby 本体)
  |     +-- ruby.exe
  |     +-- msvcrt-ruby210.dll
  |     +-- lib\ruby\2.1.0\...
  |     +-- lib\ruby\site_ruby\...
  |
  +-- tool.rb (ツール本体)
  +-- tool.bat (ツールを実行するバッチ)
```

tool.bat は、以下のようなバッチファイル

```bat:tool.bat
@echo off

rem PATHをリセット
set PATH=

rem デフォルトでロードするライブラリ
set libs=-renc/encdb -renc/windows_31j
rem オプション
set opts=--disable-gems

rem 方法1
pushd %~dp0
ruby\ruby.exe %libs% %opts% %~n0.rb %1 %2 %3 %4 %5 %6 %7 %8 %9
popd

rem 方法2
"%~dp0ruby\ruby.exe" %libs% %opts% "%~dpn0.rb" %1 %2 %3 %4 %5 %6 %7 %8 %9

pause
```

## tool.batの補足説明
* 個人のPCにインストールされているRubyに依存しないように、PATH環境変数を空にリセットしている。必要に応じて、C:\Windows など必要なパスは追記する。
* batのファイル名が実行するツールのファイル名になるようにして、batのひな形を使いまわせるようにしている。
* rubyを実行する戦略として、batの内容は以下の2通りがあり、状況に応じて(なんとなく)使い分けている。
  - ツールのフォルダにpushdして、実行する方法(ツールがUNCパスに存在しても、一時的にドライブを割り当ててローカルパスで実行できる)
  - カレントパスを移動せずに、フルパスでrubyやツールを指定する方法

# 新たに発生した問題点

実は、私はちょっと前までWindows環境ではrubyバージョン1.8を利用しつづけており上記の方法で問題なかった。
しかし、バージョン1.9や2.1で同じ方法を行うと以下の問題点に遭遇した

* rubygems.rb がないと言ってエラーになる(エラーメッセージ: ``<internal:gem_prelude>:1:in `require': cannot load such file -- rubygems.rb (LoadError)``)
* 上記を回避するために、`--disable-gems`オプションをrubyの引数に指定すると今度は、Windows-31Jがわからないといってエラーになる(エラーメッセージ: ``ruby\ruby: unknown encoding name - Windows-31J (RuntimeError)``
* 上記を回避するために、また、`coding: Windows-31J` のようなスクリプトエンコーディングを有効にするため、さらに、`coding: cp932` のようにエイリアスを有効にするために以下のライブラリが必須。かつ、オプション-rで事前にロードする。

```
ruby/lib/ruby/2.3.0/i386-mingw32/enc/windows_31j.so
ruby/lib/ruby/2.3.0/i386-mingw32/enc/encdb.so
```

これでもまだ問題はあって、ツールを日本語名のフォルダに格納すると、enc/encdb.so がロードできないと言ってエラーになる。(``test.rb:1:in `require': cannot load such file -- enc/encdb (LoadError)``)

とりあえず、配布するツールは日本語を含んだフォルダに置かないようにお願いするのがひとつの解決策である。

以降は、根本対策としてRuby自体を変更してこの問題を解決することを考える。

# Windows環境でのRubyビルド手順

まず、Windows環境で普通にrubyをビルドするための手順をメモとして残す。

## 参考

[mingw な ruby2.0 を cygwin 上でビルドする](http://qiita.com/hidachinoiro/items/19d2083ecc1bb72e3ccd)

## 普段Cygwinを使っている人向けの事前準備

Cygwinやapt-cygは事前にインストールしておくこととし、以下で必要なパッケージのインストールを行う。

```
$ apt-cyg install mingw64-x86_64-gcc-g++ autoconf make
```

※ 現在、mingw用のクロスコンパイラとしては、[Mingw-w64](http://mingw-w64.sourceforge.net/) が良いらしい。そのため、インストールするパッケージを mingw-gcc-g++ から、mingw64-x86_64-gcc-g++ に変更した。なお、Rubyのビルドにc++は不要。

## コンパイル環境がない人向けの事前準備

[DevKit](http://rubyinstaller.org/downloads/)をインストールするのが簡単そう。

DevKitの場合、後の手順のconfigure時にホストになるrubyを要求されるので、ruby自体のインストールも必要になる(入ってる前提として手順は省略する)

32ビット版なら、
`DevKit-mingw64-32-4.7.2-20130224-1151-sfx.exe`
64ビット版なら、
`DevKit-mingw64-64-4.7.2-20130224-1432-sfx.exe`
をダウンロード、展開する。(例えば、C:\DevKitに展開したとする)

C:\DevKit\msys.bat を実行して、起動したシェルで以降の作業を行う。

## Rubyソースを取得

Rubyの[ダウンロードページ](https://www.ruby-lang.org/ja/downloads/)からソースを取得する場合

```shell-session
$ tar xvzf ruby-2.2.1.tar.gz
cd ruby-2.2.1
```

Gitで最新ソースを取得する場合(Gitの環境は省略する)

```shell-session
$ git clone https://github.com/ruby/ruby.git
$ cd ruby
$ autoreconf
```

## configure && make
```shell-session
$ mkdir build
$ cd build

$ ../configure --enable-shared --prefix=/usr/local

# 64ビットマシンで32ビット用バイナリをクロスコンパイルする場合のconfigureは以下
$ ../configure --target=i686-pc-mingw32 --host=x86_64-w64-mingw32 --enable-shared --prefix=/usr/local

$ make EXTS=date,pathname DOCTARGETS= INSTALLDOC=nodoc DESTDIR=~/xxx install
```

* `EXTS=date,pathname` は、コンパイル時間短縮のためにmakeする拡張ライブラリを限定している。ソースディレクトリのext配下のディレクトリ名をカンマ区切りで指定する。
* `DOCTARGETS=` は、時間のかかるrdocの生成を無視する。よく、configure時に--disable-install-docを指定する例を見かけるが、このような制御はmake時の方が好み。
* `INSTALLDOC=nodoc` は、install時にrdocやriのインストールを無視するために指定する。ここでは、DOCTARGETS=が空なので指定してもしなくてもよい。
* `DESTDIR=` により、指定したフォルダにruby一式がインストールされる。値はなんでもいい。この例では、$HOME/xxx/usr/local 配下にインストールされる。このmake変数は自PCにインストールはしないけれど、ディレクトリ構成を維持した配布物一式を固めるのに便利。

# Windows-31J をビルトインにする

rubyを日本語フォルダ名に配置しても問題なく動くようにするためには、ライブラリロードなしに日本語フォルダ名を解釈できるようにしなければならないようだ。そのため、以下の対応を試してみた。

* Windows-31Jをライブラリロードせずに解決できるよう、`enc/windows_31j.o`をrubyにスタティックリンクする
* CP932のエイリアスを解決するために`enc/encdb.o`をスタティックリンクする
* CP932からUTF-8へのコード変換を行う(file.c:rb_str_encode_ospath() で、この変換を行っている)ために`enc/trans/transdb.o`と`enc/trans/japanese_sjis.o`をスタティックリンクする

以下は、上記解決策を行うためのパッチとビルド手順である

```diff:
diff --git a/dmyenc.c b/dmyenc.c
index 7e006e8..a542c7a 100644
--- a/dmyenc.c
+++ b/dmyenc.c
@@ -1,10 +1,19 @@
 #define require(name) ruby_require_internal(name, (unsigned int)sizeof(name)-1)
 int ruby_require_internal(const char *, int);
 
+void Init_encdb(void);
+void Init_transdb(void);
+
 void
 Init_enc(void)
 {
     if (require("enc/encdb.so") == 1) {
 	require("enc/trans/transdb.so");
     }
+    else {
+#if !defined(MINIRUBY)
+	Init_encdb();
+	Init_transdb();
+#endif
+    }
 }
diff --git a/enc/make_encmake.rb b/enc/make_encmake.rb
index 6ea2213..e10112b 100755
--- a/enc/make_encmake.rb
+++ b/enc/make_encmake.rb
@@ -51,7 +51,7 @@ def target_encodings
   deps = Hash.new {[]}
   inc_srcs = Hash.new {[]}
   default_deps = %w[regenc.h oniguruma.h config.h defines.h]
-  db = encs.delete("encdb")
+  encs.delete("encdb")
   encs.each do |e|
     File.foreach("#$srcdir/#{e}.c") do |l|
       if /^\s*#\s*include\s+(?:"([^\"]+)"|<(ruby\/\sw+.h)>)/ =~ l
@@ -72,7 +72,7 @@ def target_encodings
   inc_srcs.each do |e, d|
     deps[e].concat(inc_srcs.expand(d))
   end
-  encs.unshift(db)
+  encs.unshift("encdb")
   return encs, deps
 end
 
@@ -93,7 +93,8 @@ def target_transcoders
   trans.uniq!
   atrans = atrans.sort_by(&ALPHANUMERIC_ORDER)
   trans = trans.sort_by(&ALPHANUMERIC_ORDER)
-  trans.unshift(trans.delete("transdb"))
+  trans.delete("transdb")
+  trans.unshift("transdb")
   trans.compact!
   trans |= atrans
   trans.map! {|e| "trans/#{e}"}
diff --git a/encoding.c b/encoding.c
index fd3344b..2f7a221 100644
--- a/encoding.c
+++ b/encoding.c
@@ -582,6 +582,9 @@ rb_enc_init(void)
     ENC_REGISTER(ASCII);
     ENC_REGISTER(UTF_8);
     ENC_REGISTER(US_ASCII);
+#if !defined(MINIRUBY)
+    ENC_REGISTER(Windows_31J);
+#endif
 #undef ENC_REGISTER
 #define ENCDB_REGISTER(name, enc) enc_register_at(ENCINDEX_##enc, name, NULL)
     ENCDB_REGISTER("UTF-16BE", UTF_16BE);
@@ -593,7 +596,9 @@ rb_enc_init(void)
     ENCDB_REGISTER("UTF8-MAC", UTF8_MAC);
 
     ENCDB_REGISTER("EUC-JP", EUC_JP);
+#if defined(MINIRUBY)
     ENCDB_REGISTER("Windows-31J", Windows_31J);
+#endif
 #undef ENCDB_REGISTER
     enc_table.count = ENCINDEX_BUILTIN_MAX;
 }
diff --git a/internal.h b/internal.h
index 6d480ee..c3206ee 100644
--- a/internal.h
+++ b/internal.h
@@ -754,6 +754,7 @@ VALUE rb_math_sqrt(VALUE);
 
 /* newline.c */
 void Init_newline(void);
+void Init_japanese_sjis(void);
 
 /* numeric.c */
 int rb_num_to_uint(VALUE val, unsigned int *ret);
@@ -1017,6 +1018,9 @@ char *ruby_hdtoa(double d, const char *xdigs, int ndigits, int *decpt, int *sign
 /* utf_8.c */
 extern rb_encoding OnigEncodingUTF_8;
 
+/* windows_31j.c */
+extern rb_encoding OnigEncodingWindows_31J;
+
 /* variable.c */
 size_t rb_generic_ivar_memsize(VALUE);
 VALUE rb_search_class_path(VALUE);
diff --git a/transcode.c b/transcode.c
index 17d406e..d3e1492 100644
--- a/transcode.c
+++ b/transcode.c
@@ -4552,4 +4552,7 @@ InitVM_transcode(void)
     rb_define_method(rb_eInvalidByteSequenceError, "incomplete_input?", ecerr_incomplete_input, 0);
 
     Init_newline();
+#if !defined(MINIRUBY)
+    Init_japanese_sjis();
+#endif
 }
```

```bash:build.sh
#!/bin/sh

set -ex

test -d build || mkdir build
cd build

# configure を実行
test -f config.status || ../configure --target=i686-pc-mingw32 --host=x86_64-w64-mingw32 --prefix=/usr/local

# encdb.h transdb.h を作成する
make \
  cppflags=-DMINIRUBY \
  miniruby \
  encdb.h \
  transdb.h

# -DMINIRUBY 付きでコンパイルしたオブジェクトを削除する
rm -f encoding.o
rm -f dmyenc.o
rm -f transcode.o

# ruby をビルドする
make EXTS=date,pathname \
  DOCTARGETS= \
  Q= \
  BUILTIN_ENCOBJS="enc/ascii.o enc/us_ascii.o enc/unicode.o enc/utf_8.o enc/windows_31j.o enc/encdb.o" \
  BUILTIN_TRANSOBJS="enc/trans/newline.o enc/trans/japanese_sjis.o enc/trans/transdb.o"
```

Windows-31Jをビルトインにするこの方法は、ちょっと試す限りではうまく行く。しかし、ビルドの仕方が無理やりなのが気になる。

# ASCII-8BITのパスをロードできるようにする

別の解決策として、デフォルトのファイルシステムエンコーディングが定まらないときは、ライブラリロード時のパスをASCII-8BITで解釈する変更を考える。

※注: このパッチはまだ不完全

```diff:
diff --git a/dln.c b/dln.c
index 355cec1..658d434 100644
--- a/dln.c
+++ b/dln.c
@@ -1261,12 +1261,13 @@ dln_load(const char *file)
     char message[1024];
     void (*init_fct)();
     char *buf;
+    extern UINT rb_w32_filecp(void);
 
     /* Load the file as an object one */
     init_funcname(&buf, file);
 
     /* Convert the file path to wide char */
-    winfile = rb_w32_mbstr_to_wstr(CP_UTF8, file, -1, NULL);
+    winfile = rb_w32_mbstr_to_wstr(rb_w32_filecp(), file, -1, NULL);
     if (!winfile) {
 	dln_memerror();
     }
diff --git a/file.c b/file.c
index 44cf4ae..28bb7af 100644
--- a/file.c
+++ b/file.c
@@ -241,6 +241,10 @@ rb_str_encode_ospath(VALUE path)
     if (enc == rb_ascii8bit_encoding()) {
 	enc = rb_filesystem_encoding();
     }
+    if (enc == rb_usascii_encoding() || enc == rb_ascii8bit_encoding()) {
+      /* return raw encoding path */
+      return path;
+    }
     if (enc != utf8) {
 	path = rb_str_conv_enc(path, enc, utf8);
     }
@@ -3776,7 +3780,12 @@ realpath_rec(long *prefixlenp, VALUE *resolvedp, const char *unresolved, VALUE l
 #ifdef __native_client__
                 ret = stat(RSTRING_PTR(testpath2), &sbuf);
 #else
-                ret = lstat(RSTRING_PTR(testpath2), &sbuf);
+                if (rb_enc_get(testpath2) == rb_ascii8bit_encoding()) {
+                  ret = rb_w32_stati64(RSTRING_PTR(testpath2), &sbuf);
+                }
+                else {
+                  ret = lstat(RSTRING_PTR(testpath2), &sbuf);
+                }
 #endif
                 if (ret == -1) {
                     if (errno == ENOENT) {
diff --git a/win32/file.c b/win32/file.c
index 0fddaaa..56d569a 100644
--- a/win32/file.c
+++ b/win32/file.c
@@ -339,6 +339,9 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na
     }
 
     cp = path_cp = code_page(path_encoding);
+    if (path_encoding == rb_ascii8bit_encoding()) {
+        cp = path_cp = system_code_page();
+    }
 
     /* workaround invalid codepage */
     if (path_cp == INVALID_CODE_PAGE) {
@@ -646,7 +649,7 @@ rb_file_load_ok(const char *path)
     long len;
     wchar_t* wpath;
 
-    wpath = mbstr_to_wstr(CP_UTF8, path, -1, &len);
+    wpath = mbstr_to_wstr(system_code_page(), path, -1, &len);
     if (!wpath) return 0;
 
     attr = GetFileAttributesW(wpath);
```