C++
Node
Make

NodeでC++プログラムをコンパイルする。

More than 1 year has passed since last update.


何故、nodeか?

Makefileはもう書きたくない。でもCMakeを新しく覚えるのも嫌だ...。というか回りにあるCMakeLists.txtがひどいから読みたくない...。それなら軽量スクリプト言語から自分でかけばいいじゃんとなった。そしてJavaScriptオブジェクトの自由度の高さから設定ファイルが楽にかけるんじゃないか?と思ったのでnodeを選択した(単に最近JavaSciptを書いているからというのが一番な理由な気がする。)


configファイルの読み込み

最初、json形式にしようと思ったが、json形式だと関数が設定できないのでwebpackを見習ってmake.config.jsとして渡せるようにした。


make.config.js

module.exports={

compiler: "g++",
options: [ '-std=c++17',
'-Wall', '-Wextra',
'-O3', '-pthread', '-m64',
'-I./include/',
],
libs: [],
src_dir: 'src',
obj_dir: 'obj',
main_dir: 'main',
exec_dir: 'bin',
pre_exec: function(){
console.log("===== Pre Process jsmake =====");
console.log(this);
console.log("===== Pre Process jsmake =====");
}
}

設定はコンパイラ、ソースディレクトリ、オブジェクトディレクトリ、main関数用ディレクトリと実行ファイルディレクトリにした。オプションやライブラリは配列形式で指定する。

pre_execは今は何もしていないがシェル実行でライブラリなどを追加する際に使う関数としている。


mainの実行プログラム


jsmakecpp

#!/usr/bin/env node

// -*- coding:utf-8 mode:js -*-
const fs=require('fs');
const path=require('path');
const child_process=require('child_process');
const getAllFiles=require('./node_modules/getAllFiles.js');
const replaceExt=require('./node_modules/replaceExt.js');
const replaceDir=require('./node_modules/replaceDir.js');
const { StringDecoder } = require('string_decoder');

(function main(){
console.log("===== compile cpp by node START =====");
const config=getConfig();
if( config.pre_exec!=null && typeof config.pre_exec==='function' ) config.pre_exec();
makeObj(config);
})();

function getConfig(){
let configFileName=process.argv.length[2];
if( configFileName==null ) configFileName='make.config.js';
const absPath=path.resolve(process.cwd(), configFileName);
const config=require(absPath);

return config;
}


コマンドらしく拡張子はつけなかった。emacsでは#!/usr/bin/env nodeではJavaScriptと認識してくれなかったので2行目// -*- coding:utf-8 mode:js -*-でモードを指定している。設定ファイルはコマンドラインからも読み込めるようにしているがこれが本当に良い実装かは検討中。

const config=require(path)とすることでexportされたモジュールとして読み込める。これは完全なJavaScriptオブジェクトにできるので関数も読み込める。

pre_exec要素はmain部分でチェックして関数なら実行している。どうしてもこういう泥臭いチェックするのが一番だと思う。


Objectファイル生成部分


makeObj.js

function getFlags(config){

let flags=' ';
for( const op of config.options ) flags+=op+' ';
return flags;
}

function makeObj(config){
const files=getAllFiles(path.join(process.cwd(), config.src_dir));
for( const p of files.filter((a)=>{ return (path.extname(a)==='.cc' || path.extname(a)==='.cpp'); }) ){
const src=path.relative(process.cwd(), p);
const out=replaceExt(replaceDir(src, config.src_dir, config.obj_dir), '.o');
cmd=config.compiler+' '+getFlags(config)+' -c -o '+out+' '+src
try{ child_process.execSync(cmd); }
catch(e){
console.log('compile ', src, ' to', out, ' error');
throw new Error('c++ compile error');
}
console.log('src : ', src, ' out : ',out);
}
}


getAllFilesreplaceExtrepaceDirは再利用性が高いと考え外部関数として定義した。


util.js

const fs=require('fs');

const path=require('path');

// getAllFiles.js
function getAllFiles(name){
name=path.resolve(name);

const result=[];
const files=fs.readdirSync(name);
for( let name2 of files ){
name2=path.resolve(name, name2);
if( !fs.existsSync(name2) ) continue;

if( fs.statSync(name2).isDirectory() ) for( const a of getAllFiles(name2) ) result.push(a);
else result.push(name2);
}
return result;
}
module.exports=getAllFiles;

// replaceExt.js
function replaceExt(p, ext){
const obj=path.parse(p);
const name=obj.name+ext;
const root=path.format({ dir: obj.dir, base: name });

return root;
}
module.exports=replaceExt;

// replaceDir.js
function replaceDir(p, before, after){
if( before.indexOf('./')===0 ) before=before.replace('./' ,'');
if( after.indexOf('./')===0 ) after=after.replace('./' ,'');
return p.replace(before, after);
}
module.exports=replaceDir;


getAllFilesは再帰的にディレクトリを探しファイルをリストにして返す関数である。最初はfilterも引数にしコマンドのfindっぽくしてしまおうかと考えたが、リストで返すのでリストに対するfilter処理のほうがわかりやすく自由度が高いと思い。ただ単にファイルのリストを返す関数にした。

replaceExtは文字列操作で置き換えでも良いかと思ったがせっかくpathモジュールがあるのでそれを使うことにした。

replaceDirはwebpack.config.jsでは./が省略できないがfsではつけていると置き換えるファイルが見つからなくなるので文字列の先頭についていれば削除することにした。

(どのようにして./が付いているとエラーでなく、つかないとエラーになるのかは不明である...。)


実行ファイル生成部分


makeExe.js

function makeExe(config){

const files=getAllFiles(path.join(process.cwd(), config.main_dir));
for( const p of files.filter((a)=>{ return (path.extname(a)==='.cc' || path.extname(a)==='.cpp'); }) ){
console.log("===== make exec file =====");
const src=path.relative(process.cwd(), p);
const out=replaceExt(src, '.o');
let cmd=config.compiler+' '+getFlags(config)+' -c -o '+out+' '+src
try{ child_process.execSync(cmd); }
catch(e){
console.log('compile ', src, ' to', out, ' error');
throw new Error('c++ compile error');
}
const objs=getAllFiles(path.join(process.cwd(), config.obj_dir)).filter((a)=>{ return path.extname(a)==='.o'; });
objs.push(out);
const exefile=replaceDir(replaceExt(src, ''), config.main_dir, config.exec_dir);
let cmd2=config.compiler+' '+getFlags(config)+' -o '+exefile+' ';
for( const o of objs ) cmd2+=path.relative(process.cwd(), o)+' ';
cmd2+=getLibs(config);

console.log('make ', exefile);
try{ child_process.execSync(cmd2); }
catch(e){
console.log('make ', exefile, ' error');
throw new Error('c++ compile error');
}
}
}


main関数を含むobjをほかと混ぜるとリンク時に間違うのでmainディレクトリに作った。(filterなどで取り除く操作も考えられるが今回はこれで行く)。

実行ファイルはmain関数をコンパイルしたものと前に作っておいたオブジェクトファイルをリンクさせることで作る。


今回の実装

nodeでmakeシステムを作ってみた。

JavaScriptファイル(make.config.js)を設定ファイルとして読み込めるようにした、完全なJavaScriptなので関数も書ける。ただし呼び出しはメインの処理でしないといけない。今回はpre_execのみだがpost_exec等をつけても良いかもしれない。この技は有用性があると思う。

内部実装としてはgetAllFiles(path)は汎用性が高そう、nodeのpathライブラリは絶対パス指定、相対パス指定の切り替えが楽、ただしどっちで扱ってるか間違えると痛い目にあう。

webpackのカレントディレクトリ指定の./は謎、一応、対応したがただ無視しているだけなのでバグの温床にならないかは心配、そういえばrequireimportのパス指定も./が必要だった。

現在のワーキングディレクトリの取得はprocess.cwd()、コマンドだとpwdでこの実装がよく使われているので注意。

結局すべて同期実行SyncでJavaScriptらしくないライブラリであるがそこまで時間短縮したいプログラムでないので良しとする、逆に中途半端に非同期実行を混ぜるほうがわかりにくくなりそうな気がする。同期実行のnodeでコマンドライクなユーティリティを書くのは意外と使えそうな気がする。


次に向けて

今回書いたプログラムはgetAllFiles,replaceExt,repaceDir以外はすべて1ファイルに書いてあるのでいくつか関数化して外に追い出したい、特にsrcからobj部分が重複しているので関数化はしたほうが良さそうである。

エラー処理がエラーを握りつぶしてエラーが起こったことだけを報告しているひどい実装なので要改善

これは同期実行のexecSyncがバイト列データでエラーを返していることと、例外オブジェクトが巨大すぎること、コンパイルエラーが膨大になることなどにより実装が難しくなっている。

ファイルの更新チェックなどをしていないのですべてのファイルをコンパイルするである。これはファイル更新時間を取得して比較すれば解消するはずだが依存ファイルを調べるとなると骨が折れそうなので後回しにした。


まとめ

未だ完全な実装ではないがnodeを使ったmakeシステムを作ってみた。ユーティリティ系を関数化することでシェルスクリプトでは中途半端だった配列に対する処理やJavaScriptオブジェクトの自由度を活かした処理などができそうである。ただしfspathなどのライブラリやコマンドライン引数にはクセがあるのでそれをしっかり抑えておく必要がある。

タグにC++と書いたがC++プログラムは一行もでなかった。