DOAP2023開発日記 #7
タスクランナーGulpを使う
「タスクランナー」というと、Webアプリをパッケージングするイメージが強いかもしれない。タスクランナー専用のツールでは、「Gulp」や「Grunt」などがあるが、「npm-scripts」のようなパッケージャ付属のものや、「VSCodeタスク」のようなIDE付属のもの、古来からのシェルスクリプトやバッチファイルなども含めると、世の中はタスクランナーで溢れていることに気付く。肝要なのは、長所と短所を見極めて、適切なものを使うことではないかと思う。
話を戻して、私はDOAP2023も含め、最近のプロジェクトで使用するタスクランナーはGulp一択になっている。一番の理由は何と言ってもnode.jsで使用できること。ES2015(ES6)の書式に慣れてくると、Gulpでの設定コーディングが書きやすいことこの上ない。
Gulpが便利だなと実感したのは、開発したReactアプリをSailsサーバにデプロイする時だった。Sailsのassetsディレクトリを削除し、react-scriptsのデプロイコマンドでパッケージングし、そのディレクトリをSailsのassetsに移動する作業を難なくこなしてくれた。
次にGulpを適用してみたのは、Qtプロジェクトでできあがったアプリケーションやライブラリを、Qt Installer Frameworkを使ったインストーラ作成だった。インストーラを作るには、成果物の他に依存するライブラリ(QtやVC、OpenSSL等)を同梱しなければならず、それらをそれぞれの場所からコピーする必要がある。バージョンなどが決まればライブラリはそれほど変化することはないが、32/64ビット版やデバッグ/リリース版を切り替えてパッケージする必要がある場合は気が狂いそうになる。そんな作業切替も、Gulpタスクランナーが助けてくれる。バックにあるnode、数多あるnpmパッケージ、ES6コーディングの貢献が大きい。
今回は、Gulpでタスクランナーを書く時に、知っておくと便利なnode.jsやnpmパッケージについてのTipsを紹介する。
OS判定
マルチプラットフォームでコンパイルするなら、各OSで異なる書き方をしなければいけない場面に度々遭遇する。そんな時は、node.js標準の process.platform を使う。例えば、こんな風に使える。
const os = {
win: () => process.platform === 'win32',
mac: () => process.platform === 'darwin',
lnx: () => process.platform === 'linux',
};
例えば次のようにして、現在のプラットフォームを判定することができる。
if (os.win()) console.log('This is Windows!');
ディレクトリの再帰的削除
こちらの記事を参考に、Promise版で実装してみた。
const path = require('path');
const {
existsSync,
readdir,
stat,
unlink,
rmdir,
} = require("fs-extra"); // node標準の"fs"でも可
async function removeDirRecursive(dir) {
// 指定されたディレクトリが存在しない場合は何もせずに終了
if (!existsSync(dir)) return;
// ディレクトリ内のファイル/ディレクトリリストを取得
const files = await readdir(dir);
for (const file of files) {
// パスを作成
const filePath = path.join(dir, file);
// パスが示す情報を取得
const fstat = await stat(filePath);
// パスがディレクトリなら
if (fstat.isDirectory()) {
// 先に中を削除
await removeDirRecursive(filePath);
}
// パスがファイルなら
else {
// ファイルを削除
await unlink(filePath);
}
}
// ディレクトリを削除
await rmdir(dir);
}
例えば、次のようにしてhogeディレクトリを削除できる。
await removeDirRecursive('hoge');
qmakeでメインプロジェクトのMakefileを作成する
Windows版/Linux版、デバッグ/リリースビルドの違いを吸収して、指定したプロジェクトファイルから最初のMakefileを作成する。
const util = require('util');
const childProcess = require('child_process');
const exec = util.promisify(childProcess.exec);
async function makeProjectMakefileFn({
QtBin,
makefileDir, // Makefileを作成する先
projectPath, // メインプロジェクトファイル(.pro)のパス
env, // 追加/上書きする環境変数のオブジェクト
debug, // デバッグ版ならtrue
}) {
const spec = os.win() ? 'win32-msvc' : 'linux-g++';
const cmd = [
QtBin.qmake, // qmakeへの絶対パス
`-o ${makefileDir}/Makefile`,
projectPath,
`-spec ${spec}`,
debug
? '"CONFIG+=debug" "CONFIG+=qml_debug"'
: '"CONFIG+=qtquickcompiler"',
].join(' ');
await exec(cmd, {
env: { ...process.env, ...env }
});
}
特にWindowsの場合、envのPATHにはVSC++で必要なパスが通っていることが望ましいので、Visual Studioの「x64/86 Native Tools Command Prompt for VSXX」のPATH環境変数の内容をコピーして、envオブジェクトを通して上書きするといい。
例えば、Windowsで、Qtは5.14.2、Visual Studio 2019を使い、64ビット、デバッグビルドでMakefileを作りたい場合は以下のようになる。
await makeProjectMakefileFn({
QtBin: {
qmake: 'C:/Qt/5.14.2/msvc2019_64/bin/qmake.exe'
},
makefileDir: 'C:/Users/myhome/projects/build-myproject',
projectPath: 'C:/Users/myhome/projects/myproject/myproject.pro',
env: {
PATH: `${...VSC++2019 64ビット用のPATH...};${process.env.PATH}`
},
debug: true,
});
MakefileからサブプロジェクトのMakefileを作成する
メインプロジェクトからMakefileを作っても、.proの内容がサブディレクトリプロジェクトで定義されていれば、まだビルドすることはできない。さらにもう一段階ステップを踏んで、サブプロジェクト用のMakefileを作成する必要がある。
async function makeSubprojectMakefileFn({QtBin, cwd}) {
const cmd = [
os.win() ? QtBin.jom : 'make',
'-f',
`Makefile`,
'qmake_all',
].join(' ');
await exec(cmd, { cwd });
}
先の例でMakefileを作った場合、次のようにする。
await makeSubprojectMakefileFn({
QtBin: {
jom: 'C:/Qt/Tools/QtCreator/bin/jom/jom.exe'
},
cwd: 'C:/Users/myhome/projects/build-myproject'
});
サブプロジェクト用のMakefileでビルドする
サブプロジェクト用のMakefileができたら、これを元にターゲットをビルド(コンパイル、リンク作業)する。
async function buildSubprojectFn({QtBin, project, buildDir, env, debug}) {
const encoding = os.win() ? 'shift-jis' : 'utf-8';
const cmd = [
os.win() ? QtBin.jom : 'make',
os.win()
? `-f Makefile.${debug ? 'Debug' : 'Release'}`
: '-j6'
,
].join(' ');
for (const sub of project.subprojects) {
console.log('Build', sub);
try {
await exec(cmd, {
cwd: `${buildDir}/${sub}`,
env: { ...process.env, ...env },
encoding,
});
}
catch (e) {
const {stdout} = e;
const iconv = require('iconv-lite');
console.error(iconv.decode(stdout, encoding));
throw e;
}
}
}
Qt Creatorのビルド作業を模倣するならば、Windowsではビルドに「jom」を使うが、結果的にはVisual C++のツールが使われる。LinuxではGCCのmakeを使う。
Windows系コマンドの出力Shift-JIS問題
Visual C++コマンドラインツールがコンパイルやリンクに失敗すると、少なくとも2017や2019では、エラーメッセージがShift-JIS(CP932?)で出力される。これをそのままコンソールに出力しても文字化けしてしまうことがある。そんな時は、 process.exec のオプション encoding を用いて、受け取る文字コードを指定することで正しく受け取ることができる。
受け取ったシフトJISのテキストは 文字コード変換パッケージ(ここでは iconv-lite)でUnicode(UTF-8)にして、コンソールに表示する。
例えば、先の例で、メインプロジェクトの下に firstとsecond、2つのサブプロジェクトがあるとすると、以下のようになる。
await buildSubprojectFn({
QtBin: {
jom: 'C:/Qt/Tools/QtCreator/bin/jom/jom.exe'
},
project: {
subprojects: ['first','second']
},
buildDir: 'C:/Users/myhome/projects/build-myproject',
env: {
PATH: `${...VSC++2019 64ビット用のPATH...};${process.env.PATH}`,
INCLUDE: `${...VSC++2019 64ビット用のINCLUDE...}`,
LIB: `${...VSC++2019 64ビット用のLIB...}`,
LIBPATH: `${...VSC++2019 64ビット用のLIBPATH...}`,
},
debug: true
});
ちなみに、env.INCLUDE、env.LIB、env.LIBPATHはおまじないとして入れている。もしかしたら不要かもしれない。
まとめ
Gulpタスクランナーで使えそうなTips、スニペットを紹介した。
私自身は、ビルドの他にQtインストーラフレームワークを使ってインストーラまで作成するタスクランナーを自作して、ビルドからインストーラ作成までを、x86/x64、デバッグ/リリースの各パターンまでも一気通貫で走らせている。Qt Creatorでは味わったことのない疾走感である。
ちなみに、QtインストーラフレームワークはXMLベースの設定ファイルを使う。XML生成ができるパッケージ(xmlbuilder2など)を使えば、オンデマンドでXMLを生成できる。例えばリリース日をビルドした今日の日付にしたい時でも、その場でXMLに埋め込むことができるのは、タスクランナーのおかげだ。
Qtインストーラフレームワークは、QtだけあってWindows/Mac/Linuxなどマルチプラットフォーム対応なのだが、私の知る限り、GUI環境のないLinuxでは使えない(はず)。
そこで、Gulp/Node.jsを駆使し、ファイルを集めてTarballを作り、インストール/アンインストールするシェルスクリプトを生成し、コンソールだけでインストール/アンインストールできるところまで自作した。
タスクランナーは、ものによって長所、短所があることは冒頭に述べたが、ここまでできるのであれば、やはり私にとってはGulp一択ということになる。
この記事が気に入ったらサポートをしてみませんか?