最近のビルドツールって何なの?

TypeScriptでは、コンパイルが必要です。プログラムをブラウザーとNode.jsの両方で使おうとすると、さらに加工が必要です。ミニファイだの文書も作るだのすると、ちょっとしたビルドプロセスとなるので手作業では辛くなります。

今更Makeでもないよなー、と思い、最近のビルドツールを試してみました。

内容:

  1. 流行りすたりが激しすぎる
  2. gulpを使ってみる:こんなサンプル
  3. gulpのビルドスクリプト
  4. タスクランナーってのはビルドツールとは違うのか?
  5. ビルドツールは進化したのか

参考資料:

  1. 例題のファイルとコマンドの一覧
  2. ソースファイル

追加の話:

流行りすたりが激しすぎる

「確かGruntってツールがあったよな」と、インストールと使い方を調べていると、やたらにgulpって単語が目立つんですよね。Gruntのライバルの新興勢力らしいです。

「Grunt、ありゃもうオワコン、これからはgulpだぜぃ」みたいな記事がイッパイあるんです。でも、gulpの台頭って、ここ1年くらいの話ですよね。それまでは「Gruntスゲー、Gruntまじ便利」とか騒いでいましたよね。

わずか1、2年の期間でそこまで衰退しちゃうの? まるで一発屋芸人の世界みたい。こういう状況だと、今はチヤホヤされているgulpも、いつまでモツのかなー、と不安になります。「日本エレキテル連合」が一時的なバブルで、「8.6秒バズーカー」は息の長い芸人になると考えますか? 熱心なファンを除けば、「あいつらもたぶん…」でしょ*1

ビルドツールって、地味ながらも開発の基盤を支える重要なツールです。それを取っ替え引っ替えしてたら、移行のコストがバカにならないでしょうよ*2。ビルドツールを芸人さんで言えば、サンマやダウンタウンのような看板ではないが、関根勤さんみたいな存在であるべきです(って、この喩えが適切か疑問だけど)。Makeなんて、内海桂子師匠みたいなもんですよ(この喩えも(略)。

gulpを使ってみる:こんなサンプル

印象だけでものを言ってはいけません。gulpを使ってみることにします。でも、この記事はgulpの使い方を説明することが目的ではありません。Gruntは使ったことないので比較もできません。とりあえず、使用するサンプルは次の図のようなものです。

元になるファイル(ピンク色)は、greeter.ts, hello.ts, Hello.md の3つで、この記事の末尾の「続きを読む」に貼り付けてあります。その他のファイルはツールで生成されるもので、黄色は中間ファイル、水色が配布物になるファイルです。図で辺ラベルになっている tsc, brow, min, doc, apidoc が生成のために実行する作業(タスク)で、すべてシェル・コマンドラインから実行可能です。これらのシェル・コマンドに関する詳細も末尾の「続きを読む」にあります。

tsc, brow, min, doc, apidoc に対応する作業の手順をgulpfile.jsというJavaScriptファイルに書きます。ビルドスクリプトの記述言語が汎用言語なのはいいですね。新しいマイナー言語を憶える必要ないし、いざとなったら何でも出来るし。

gulpのビルドスクリプト

gulpfile.jsは次のようにまります*3

var gulp = require("gulp");

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

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

// brow: ブラウザー化
// browserify js/greeter.js js/hello.js -o lib/hellolib.js
var browserify = require('browserify');
var source = require('vinyl-source-stream');

gulp.task('brow', function() {
    var bundler = 
        browserify({entries: ['./js/greeter.js', './js/hello.js']})
    return bundler.bundle()
        .pipe(source('hellolib.js')) // 出力ファイル名
        .pipe(gulp.dest('./lib/')); // 出力ディレクトリ
});


// min: ミニファイ
// uglifyjs lib/hellolib.js -o lib/hellolib.min.js
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');

gulp.task('min', function() {
  return gulp.src('./lib/hellolib.js')
    .pipe(uglify())
    .pipe(rename('hellolib.min.js')) // これがないと破壊的上書き
    .pipe(gulp.dest('./lib/'));
});

// doc: HTML文書生成
// pandoc Hello.md -o doc/Hello.html
var pandoc = require('gulp-pandoc');

gulp.task('doc', function() {
  gulp.src('./Hello.md')
    .pipe(pandoc({
      from: 'markdown', // 必須
      to: 'html5',      // 必須
      ext: '.html',     // 必須
      args: ['--smart'] // 必須、空文字も不可!
    }))
    .pipe(gulp.dest('./doc/'));
});

// apidoc: APIリファレンス生成
// typedoc --mode file --module commonjs greeter.ts hello.ts -out apidoc
var typedoc = require("gulp-typedoc");

gulp.task("apidoc", function() {
    return gulp
        .src(['./greeter.ts', './hello.ts'])
        .pipe(typedoc({ 
          mode: "file",
          module: "commonjs", 
          out: "./apidoc/", 
        }))
    ;
});

末尾の参考資料に列挙してあるシェル・コマンドライン(gulpfile.jsにもコメントとして埋め込んである)をそのままJavaScript/gulp構文に翻訳したものです。しかし、コマンドラインを機械的に変換できるわけではなくて、次のような注意が必要でした。

多くの人が、gulpfile.jsはGruntのビルドスクリプトより短く簡単だと賞賛しているのですが、これが短く簡単なの? プラグインをインストールして、その使い方を調べて、ときにハマりながら*4、やっていることはコマンドラインの引き写しでしょ。だったら、コマンドラインをそのまま使えばいいんじゃないの。ずっと短くて簡単だよ。

#!/bin/sh

case "$1" in
    "tsc")
	tsc --noImplicitAny --module commonjs greeter.ts hello.ts --outDir js
	;;
    "brow") 
	browserify js/greeter.js js/hello.js -o lib/hellolib.js
	;;
    "min")
	uglifyjs lib/hellolib.js -o lib/hellolib.min.js
	;;
    "doc") 
	pandoc Hello.md -o doc/Hello.html
	;;
    "apidoc") 
	typedoc --mode file --module commonjs greeter.ts hello.ts -out apidoc
	;;

    *)
	echo "Usage: $0 <task>"
	echo " task: tsc, brow, min, doc, apidoc"
	exit 1
	;;
esac

タスクランナーってのはビルドツールとは違うのか?

シェルスクリプトでいいじゃん」に対しては、すぐさま反論があるでしょうね。

色々と高速化が期待できるってことですよね。確かにビルドが遅いのはフラストレーションだし、弊害もあります。しかし、いくら速くてもタスクを実行すれば時間がかかります。一番速いのは無駄なタスクを実行しないことです。そして、実行しない工夫こそがビルドツールの本領だと僕は思っていたのですが。

同じ内容のビルドスクリプトをmakeで書いてみます。([追記]usageもPHONYターゲットに修正しました。[/追記]

.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

このMakefileを実行してみましょう。apidocを作る例です。

$ make apidoc
typedoc --mode file --module commonjs greeter.ts hello.ts -out apidoc

Using TypeScript 1.4.1 from C:\Installed\nodist-master\bin\node_modules\typedoc\node_modules\typescript\bin
Rendering [========================================] 100%

Documentation generated at C:\Users\hiyama\Work\JsDev\gulptest\apidoc


$ make apidoc
make: Nothing to be done for `apidoc'.

$

二度目はもう実行しません。同じことをやる必要がないことを判断して Nothing to be done for `apidoc'. と言ってます。

gulpは:

$ gulp apidoc
[15:30:52] Using gulpfile ~\Work\JsDev\gulptest\gulpfile.js
[15:30:52] Starting 'apidoc'...

Using TypeScript 1.4.1 from C:\Users\hiyama\Work\JsDev\gulptest\node_modules\gulp-typedoc\node_modules\typedoc\node_modules\typescript\bin
Rendering [========================================] 100%

Documentation generated at C:\Users\hiyama\Work\JsDev\gulptest\apidoc

[15:30:55] Finished 'apidoc' after 2.55 s

$ gulp apidoc
[15:31:00] Using gulpfile ~\Work\JsDev\gulptest\gulpfile.js
[15:31:00] Starting 'apidoc'...

Using TypeScript 1.4.1 from C:\Users\hiyama\Work\JsDev\gulptest\node_modules\gulp-typedoc\node_modules\typedoc\node_modules\typescript\bin
Rendering [========================================] 100%

Documentation generated at C:\Users\hiyama\Work\JsDev\gulptest\apidoc

[15:31:02] Finished 'apidoc' after 2.58 s

$

同じことを何度でも繰り返します。いくら腕力があって爆速でも、やらない賢さには負けます。

生成されたファイルを全部消してから、makeに欲しいファイルを指定してみましょう。

$ make lib/hellolib.min.js
tsc --noImplicitAny --module commonjs greeter.ts hello.ts --outDir js
browserify js/greeter.js js/hello.js -o lib/hellolib.js
uglifyjs lib/hellolib.js -o lib/hellolib.min.js

$ make lib/hellolib.min.js
make: `lib/hellolib.min.js' is up to date.

$

makeは、lib/hellolib.min.jsを作るには、tsc、browserify、uglifyjs をこの順で実行する必要があることを、依存関係グラフをたどりながらゴールからの逆向き推論で判断して実行します。もちろん、二度目には `lib/hellolib.min.js' is up to date. と、不要なことをやろうとはしません。

gulpでも依存関係を指定できるようなのでやってみます。gulp.task('brow', ['tsc'], ...)gulp.task('min', ['brow'], ...) と変更しただけ。

$ gulp min
[15:41:57] Using gulpfile ~\Work\JsDev\gulptest\gulpfile.js
[15:41:57] Starting 'tsc'...
[15:41:58] Finished 'tsc' after 1.05 s
[15:41:58] Starting 'brow'...
[15:41:58] Finished 'brow' after 88 ms
[15:41:58] Starting 'min'...
[15:41:58] Finished 'min' after 88 ms

$ gulp min
[15:42:06] Using gulpfile ~\Work\JsDev\gulptest\gulpfile.js
[15:42:06] Starting 'tsc'...
[15:42:07] Finished 'tsc' after 1.04 s
[15:42:07] Starting 'brow'...
[15:42:07] Finished 'brow' after 81 ms
[15:42:07] Starting 'min'...
[15:42:07] Finished 'min' after 86 ms

$

必要なタスクの連鎖は理解しますが、同じことを繰り返します。作業の最適化の計画はしないようです。Antのようにプラグイン(Ant用語のタスク)側で頑張る*5のかとも思いましたが、ちょっと見た限りでは、gulpプラグインはターゲット(出力ファイル)の状態を考慮しないでひたすら作業を遂行するようです。

そう言えば、gulpはタスクランナー(task runner)と紹介されることが多いですね。(まー、ビルドツール、ビルドシステムとも呼ばれてもいますけど)。これって、タスクを実行することに特化されていて、ビルドの手間を最小にする計画性を期待するのはお門違いってこと?

ビルドツールは進化したのか

Makeは依存関係と作業の最小化について考えるし、OMakeはもっと賢く進化したツール*6です。賢くなる事がビルドツールの進化の方向なんだと僕は思っていたのですが、gulpの(たぶんGruntも)「何も考えてなさ」を見ると、この方向は受け入れられてないみたいです。少なくともWeb制作のフロントエンド界隈では、「考えている暇があるなら、四の五の言わずにやっちまえ!」方式みたいです。

ビルドスクリプトを書くのは面倒くさいので、ユーザーがビルドスクリプトを書かなくて済むようになったらいいな、とは誰でも夢想するでしょう。一種の知識ベースとして汎用ビルド規則を備えた(いちおうMakeも持っている)システムがあります。ところが一方、gulpのビルド規則再利用へのアプローチはと言えば、コピペです。

なんだか退化しているように思えるんですが、過去のビルドツールの使いにくさから考えると、「何も考えない」という割り切りも理解はできます。

ビルドツールの夢であった「賢い完全自動ビルド」は文字どおりの「夢」であって、実現できる見込みがないのなら、もう「何も考えない」のも現実的なのかも知れません。

しかしですね、gulpfile.jsが長くて書くのメンドクセー!


追加の話:

以下に参考資料:

例題のファイルとコマンドの一覧

tsc: TypeScriptコンパイル

brow: ブラウザー化

min: ミニファイ

doc: HTML文書生成

apidoc: APIリファレンス生成

ソースファイル

./greeter.ts

// file: greeter.ts

/**
 * This function greets to someone.
 */
export 
function greet(greeting: string, toWhom: string) : void {
  console.log(greeting + " " + toWhom);
}

./hello.ts

// file: hello.ts

import greeter = require('./greeter');
/**
 * This class provides a nice method to say hello.
 */
export
class Hello {
  greeting: string;
  constructor(greeting: string) {
    this.greeting = greeting;
  }
  say(toWhom: string = "World") : void {
    greeter.greet(this.greeting, toWhom);
  }
}

./Hello.md



# Hello

`Hello` is an *awesome* program
which produces a **fully-customizable**
surprisingly enchanting greeting.

./package.json

{
  "name": "HelloLib",
  "version": "0.0.1",
  "description": "fully-customizable greeting library",
  "main": "hellolib.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "hello",
    "typescript",
    "javascript"
  ],
  "author": "Masayuki HIYAMA",
  "license": "MIT",
  "devDependencies": {
    "browserify": "^9.0.8",
    "gulp": "^3.8.11",
    "gulp-pandoc": "^0.2.1",
    "gulp-rename": "^1.2.2",
    "gulp-uglify": "^1.2.0",
    "vinyl-source-stream": "^1.1.0",
    "gulp-typedoc": "^1.1.0",
    "gulp-typescript": "^2.6.0"
  }
}

*1:2、3年後にこの記事読むと意味不明かも知れません。もしその頃まで「8.6秒バズーカー」が生き残っていたら、それはそれでメデタイ話です。

*2:それとも、今どんなツールを使っているかが、コストより優先するファッションなのかな。

*3:browserifyのプラグインtsifyを使うと、tscとbrowの2つのタスクをまとめることが出来ます。

*4:入出力の取り扱いのために、streamとvinylについて知っておいたほうがいいみたいです。

*5:Antでも、頑張らないで依存性無視なタスクもあります。

*6:OMakeの開発はもう止まっている感じです。

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