gulp問題ひきずり:ウォッチがまたおバカ過ぎる

最近のビルドツールって何なの?」において、gulpは「腕力はあるが知性を持たない」、ゴレライ的というかゴライアス的というか…、そんなツールだと述べました。二、三、後から思ったことがあるので追加します。

内容:

  1. プロジェクトは肥大しちゃうことがある
  2. ウォッチをいちいち手で書くのかよー
  3. OMakeと比較してみる
  4. ウォッチはこうでなきゃ
  5. じゃ、OMakeがいいのか?

プロジェクトは肥大しちゃうことがある

「そもそも大した事はしないのでシンプルなツールgulpでも十分」ということなら、「そうですよね」と同意して話はオシマイです。でもね、gulpを使うプロジェクトが、ほんとに小さくて簡単なものなんでしょうか。

目の前に、10種のプログラミング言語が混じった2000ファイルのソースコード群がいきなり与えられたとき、「よしっ、gulpを使おう」とはならないと思うのですが、小さなプロジェクトが(良くも悪くも)大きくなることはあります。

20ファイルくらいで「gulpマジ便利」と快適に使っていて、200ファイルくらいでも「まだ大丈夫」と使い続け、2000ファイルになっても頑張る、なんてことになりませんかね。

いや、まー老婆心というやつですか、これは。

ウォッチをいちいち手で書くのかよー

最近のビルドツールって何なの?」のブックマーク・コメントに「ウォッチ(ファイルシステムの監視)をすればいい」というのがありました。確かに、リソース状態を監視する方法は、体感ビルド時間をゼロにできる可能性を秘めています。前記事の次の文はその可能性を示唆したものです。

  • ファイルの変更やバージョン管理システムへのコミットを監視して、バックグラウンドでビルドすれば、体感的な「ビルド時間」は(目標ゼロで)短縮できる。

しかし、今のgulpのウォッチは、やっぱり「知性を持たない」方法ですね。

ここから先のサンプルは、「最近のビルドツールって何なの?」と同じものを使います。ただし、TypeScriptのコンパイルには、gulp-typescriptプラグインとは別なプラグインgulp-tscを使います。理由は、gulp-typescriptが普通とは違う仕様なので*1、gulpの問題点が隠されるてしまうからです(エラー中断が起きない)。

// tsc: Typescriptコンパイル
// tsc --noImplicitAny --module commonjs greeter.ts hello.ts --outDir js
var typescript = require('gulp-tsc');

gulp.task('tsc',function(){
  return gulp.src(['./greeter.ts', './hello.ts'])
    .pipe(typescript({
      noImplicitAny: true,
      module: 'commonjs' 
    }))
    .pipe(gulp.dest('./js/'));
});

さて、ウォッチですが、TypeScriptソースファイルを監視して、変更があればtscタスクを実行するウォッチタスクを定義します。

gulp.task("watch-tsc", function() {
  gulp.watch(['./greeter.ts', './hello.ts'], ['tsc']);
});

ウォッチタスクを実行した状態で、コンパイルに失敗すると:

 gulptest > gulp watch-tsc
[15:18:07] Using gulpfile ~\Work\JsDev\gulptest\gulpfile.js
[15:18:07] Starting 'watch-tsc'...
[15:18:07] Finished 'watch-tsc' after 15 ms
[15:18:23] Starting 'tsc'...
[15:18:24] Compiling TypeScript files using tsc version 1.5.0
[15:18:25] [tsc] > C:/Users/hiyama/Work/JsDev/gulptest/hello.ts(8,1): error TS2304: Cannot find name 'exportttt'.
[15:18:25] Failed to compile TypeScript: Error: tsc command has exited with code:2

events.js:85
      throw er; // Unhandled 'error' event
            ^
Error: Failed to compile: tsc command has exited with code:2

 gulptest >

エラーでウォッチが中断してしまう、と。んで、その対策にはイディオムがあって、次のようにするんだそうです。

// tsc: Typescriptコンパイル
// tsc --noImplicitAny --module commonjs greeter.ts hello.ts --outDir js
var typescript = require('gulp-tsc');
var plumber = require('gulp-plumber'); // ← *** これを追加 ***

gulp.task('tsc',function(){
  return gulp.src(['./greeter.ts', './hello.ts'])
    .pipe(plumber()) // ← *** これを追加 ***
    .pipe(typescript({
      noImplicitAny: true,
      module: 'commonjs' 
    }))
    .pipe(gulp.dest('./js/'));
});

gulp.task("watch-tsc", function() {
  gulp.watch(['./greeter.ts', './hello.ts'], ['tsc']);
});    

実はこれでもうまくいかなくて、それは僕がチョンボしているからだと思うんですが*2、追求する気が起きないので、これでうまくいったとして話を先に進めます(苦笑)。

ということで言わせてもらうとさー、ウォッチ中に起動するかどうかでタスクを書き換えなくてはいけないのが、もうイカンですよね。それ以前にそもそもマズイのは、ウォッチタスクを人がいちいち書かなくてはならないところ。かんべんしてよ。

OMakeと比較してみる

比較のために、OMakeでウォッチしてみます。

OMakeは、GNU Makeとある程度の互換性があるので(完全互換ではない!)、「最近のビルドツールって何なの?」で出した程度の簡単なMakefileなら、一字一句直さずにそのまま使えます。ただし、名前をOMakefileに変えます。

#
# This is not Makefile, but OMakefile!
#

.PHONY : usage
usage:
	@echo "Usage: make <task>"
	@echo " task: tsc, brow, min, doc, apidoc"

.PHONY : tsc brow min doc apidoc

tsc : js/greeter.js js/hello.js
js/greeter.js js/hello.js : greeter.ts hello.ts
	tsc --noImplicitAny --module commonjs $^ --outDir js

brow : lib/hellolib.js
lib/hellolib.js : js/greeter.js js/hello.js
	browserify $^ -o $@

min : lib/hellolib.min.js
lib/hellolib.min.js : lib/hellolib.js
	uglifyjs $^ -o $@

doc : doc/Hello.html
doc/Hello.html : Hello.md
	pandoc $^ -o $@

apidoc : apidoc/index.html
apidoc/index.html : greeter.ts hello.ts
	typedoc --mode file --module commonjs $^ -out apidoc

次の1行からなるOMakerootというファイルも同じディレクトリに置きます。

.SUBDIRS: .

まずは普通にコマンドラインからomakeを実行。

 gulptest > omake tsc
*** omake: reading OMakefiles
*** omake: finished reading OMakefiles (0.02 sec)
*** omake: done (1.45 sec, 0/0 scans, 1/2 rules, 2/117 digests)

 gulptest > omake tsc
*** omake: reading OMakefiles
*** omake: finished reading OMakefiles (0.00 sec)
*** omake: done (0.02 sec, 0/0 scans, 0/2 rules, 0/19 digests)

 gulptest >

挙動はGNU Makeと同じです。二度目は何もしませんが、ちょっと時間がかかっているのは、omakeがファイルが変更されたかの確認にメッセージダイジェスト(MD5)を使っているからです。安全です。タイムスタンプを頼りにして痛い目にあった方もいるでしょ。

次に常時監視、これには -P というオプションをomakeに付けます。

 gulptest > omake -P tsc
*** omake: reading OMakefiles
*** omake: finished reading OMakefiles (0.02 sec)
*** omake: done (0.02 sec, 0/0 scans, 0/2 rules, 1/19 digests)
*** omake: polling for filesystem changes
*** omake: file hello.ts changed
*** omake: rebuilding
- build . js\greeter.js
+ tsc --noImplicitAny --module commonjs greeter.ts hello.ts --outDir js
hello.ts(8,1): error TS2304: Cannot find name 'exporttt'.
*** omake: 8/11 targets are up to date
*** omake: blocked (1.78 sec, 0/0 scans, 1/3 rules, 3/121 digests)
*** omake: targets were not rebuilt because of errors:
   js\greeter.js
      depends on: greeter.ts
      depends on: hello.ts
      - build . js\greeter.js
      + tsc --noImplicitAny --module commonjs greeter.ts hello.ts --outDir js
      hello.ts(8,1): error TS2304: Cannot find name 'exporttt'.
   js\hello.js
      depends on: greeter.ts
      depends on: hello.ts
*** omake: polling for filesystem changes
*** omake: file hello.ts changed
*** omake: rebuilding
*** omake: done (10.52 sec, 0/0 scans, 2/5 rules, 7/131 digests)
*** omake: polling for filesystem changes

メッセージがうざいので分かりにくいですが、polling for filesystem changes で監視開始、file hello.ts changed で変更を検出、rebuilding でビルド規則を適用しています。targets were not rebuilt because of errors: であっても、もちろん中断などせずに監視を続け*3、次の変更を待ちます。

ウォッチはこうでなきゃ

OMakeのウォッチとgulpのウォッチがどう違うかは明らかですよね。OMakeでは、特にウォッチ用のビルドスクリプトなんてものはありません。実行形態がバッチ的か常時監視かの違いは、ビルド規則には何ら影響がなくて、透過的なんです。必要なことは、omakeに -P オプションを付けるだけです。なんだか分かんないが、ウォッチスクリプト書きをしくじって動かないなんてトラブル(事例:檜山)は起こり得ません。

それに対してgulpのウォッチは、ユーザーによるビルドスクリプトの修正により対応します。既存タスクのあっちこっちに .pipe(plumber()) というオマジナイを挟んで修正し、次のようなウォッチタスク群を手で書き足すわけです*4

gulp.task("watch-tsc", function() {
  gulp.watch(['./greeter.ts', './hello.ts'], ['tsc']);
});    

gulp.task("watch-brow", function() {
  gulp.watch(['./js/greeter.js', './js/hello.js'], ['brow']);
});    

gulp.task("watch-min", function() {
  gulp.watch('./lib/hellolib.js', ['min']);
});    

gulp.task("watch-doc", function() {
  gulp.watch('./Hello.md', ['doc']);
});

gulp.task("watch-apidoc", function() {
  gulp.watch(['./greeter.ts', './hello.ts'], ['apidoc']);
});

これらの組み合わせであるウォッチタスクを書くのも、既存タスクが増えたり変更されたりしてウォッチタスクを書き直すのも人手。徹頭徹尾コピペの世界。労働力集約型ビルドシステム。

コピペの連鎖になる理由は、依存関係を宣言的に記述する手段を持たないからです。「最近のビルドツールって何なの?」の依存関係の図を再掲します。ちょっと小さいので見にくいときは元記事を参照してください。

この図が示す関係性をgulpはまったく知らないので、考えようにも材料がない。代わりに人間がこの図を頭のなかに(または紙に)描いて、「このリソースが変わったらこの手順で…、バッチ実行ならこうで、ウォッチのときはエーと…」と考えて手で記述するしかありません。耐えられないな、僕は。

「耐えられない」のは檜山が根性なしだからだ、というのはその通りですけど、耐えられないけど根性と技量がある人が、gulpfile.jsを生成するメタビルドツールを作るぞ、とか言い出しそうだな。それって、いつか来た道(地獄のAutotoolsとか)。

[追記]ここで言っている「リソースのあいだの依存性」は、gulpが持っている「タスクのあいだの依存性」とは別物です。リソースのあいだの依存性とは、宣言的に記述される関係(What)であって、タスクの動作記述(How)とは独立です。gulpで言っている「依存性」は一種の実行制御機構でしょう。手順記述の一部に過ぎません。

宣言的な記述としては、リソースノード上の有向グラフ構造が書けるだけではどうも不足で、状態全体が満たすべき条件を論理式で書きたいところです。[/追記]

じゃ、OMakeがいいのか?

OMakeはGNU Makeのダメなところを手直しした、すごく賢いツール。それじゃ、知性を持たない(文字通りのヘッドレスな)gulpの代わりにOMake使えってことになるのか、と言うと、OMakeもダメだと思う。検索すると「オマケ」ばっかり出ちゃうから? -- まー、それもある。

個人的には、OMakeすごく良く出来てるなーと関心しますけど、クスッって笑ってしまうところもあるんです。GNU Makeの轍を踏むまいとして、結局同じことやってらー、って感じです。

(大枠において)Makeと同じ土俵に乗って、Makeの欠点を修復しようとしている時点で、Makeの亜種ということになり、Makeの根本的な欠点は引きずることになります。「OMake素晴らしい!」って感じる人って、嫌々ながらGNU Make使っていた人だと思うんだよね。良きにつけ悪しきにつけ、Make的土壌があるところにOMakeは花咲くわけで … って、僕、なんか洒落た台詞言ってる … でも、そんなところでしょ、実際。

宣言的なビルド規則/メタビルド規則は大好きだし、まっとうだと思うけど、まっとうだから受け入れられとは限らないのです。人類はビルドの業火に炙られ続けるんじゃないの。(ちょっと寂しいオチ。)

*1:gulp-typescriptが、gulpの問題をプラグイン側で解決しようとしてるのか、それとも単なる偶然か、よく分かりません。

*2[15:33:00] Failed to compile TypeScript: Error: tsc command has exited with code:2、これに続いて、[15:33:00] Plumber found unhandled error: Error in plugin 'gulp-tsc'といって、エラーを抑制するはずのplumberごと死んでいるみたい。plumberって「ぶち壊す」って意味だったのか by『英辞郎』、なるほどね。トホホ。誰か助けて。

*3:中断したいときは、小文字の -p オプションです。

*4:これでウォッチがうまくいくことを確認してないです。もっと洒落た工夫があるかも知れませんしね。しかし、ウォッチを書きたくねーよ、って話なんでご容赦を。

コメント
0件
トラックバック
1件
ブックマーク
0 users
m-hiyama
m-hiyama