仕様変更
前回は設定ファイルを引数で受け取っていたがmake.config.js
に固定した。
代わりに引数で動きを決められるようにした、単にcleanをしたかっただけだけど
実装
#!/usr/bin/env node
// -*- coding:utf-8 mode:js -*-
const fs=require('fs');
const path=require('path');
const getAllFiles=require('./node_modules/getAllFiles.js');
const compile=require('./node_modules/compile.js');
(function main(){
const config=require(path.resolve(process.cwd(), 'make.config.js'));
if( config.pre_exec!=null && typeof config.pre_exec==='function' ) config.pre_exec();
if( process.argv[2]===void 0 ){
console.log("===== compile cpp by node START =====");
makeObj(config);
makeExe(config);
}
if( process.argv[2]==='clean' ){
console.log("===== clean =====");
for( const a of getAllFiles(config.obj_dir).filter(a=>{ return path.extname(a)==='.o' }) ) fs.unlinkSync(a);
for( const a of getAllFiles(config.main_dir).filter(a=>{ return path.extname(a)==='.o' }) ) fs.unlinkSync(a);
}
})();
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');
compile.cpp.obj(config, out, src);
}
}
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');
compile.cpp.obj(config, out, src);
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);
compile.cpp.exec(config, exefile, objs);
}
}
引数が存在しなければmake、cleanならばcleanにした。nodeのfsではファイルの削除はunlink
であるようだ命名としてはイマイチだと思う。
make.config.jsにinclude_dir属性を追加する。これはinludeされたファイルの更新日時をチェックして更新されていればコンパイルするようにするためにつけた。いわいるmake depend
のような働きをしてくれる。
そのため、flag設定を取ってくるところは
function getFlags(config){
let flags=' ';
for( const op of config.options ) flags+=op+' ';
for( const inc_dir of config.include_dir ) flags+='-I'+inc_dir+' ';
return flags;
}
のように-I
を付け足してCPPFLAGに渡す。Flagに直接-I
できるがその場合、更新チェックはされない。外部ライブラリなどはこっちで渡したほうがチェックた少なくなる。
const fs = require('fs');
const path=require('path');
function getInclude(config, filename){
const src=fs.readFileSync(filename).toString();
const list=src.split('\n').filter((l)=>{ return l.includes('include'); }).filter((l)=>{ return l.includes('"'); })
const result=[];
for( let a of list ){
a=a.replace('#include', '');
a=a.replace(/"| /g, '');
if( path.isAbsolute(a) ){
if( fs.existsSync(a) ){
for( const e of getInclude(config, a) ){
if( result.includes(e) ) continue;
result.push(e);
}
result.push(a);
}
}
else{
for( let d of config.include_dir ){
if( fs.existsSync(path.resolve(process.cwd(), d, a)) ){
for( const e of getInclude(config, path.resolve(process.cwd(), d, a)) ){
if( result.includes(e) ) continue;
result.push(e);
}
result.push(path.resolve(process.cwd(), d, a));
break;
}
}
}
}
return result;
}
module.exports=getInclude;
includeされたファイルの取得は実際にファイルを読んで存在しなければ無視する。#include <>
タイプはstdやそれに準じたレベルのものだと思うのでチェック対象にしなかった。
fs.readFile
で読んでるが返り値がarraybuffer?
なのでtoString
で文字列に戻している。その他の方法として文字コーディングを与えると文字列になるようである。また絶対パス指定にも対応させるためにpath.isAbsolute
判定を入れている。重複はfor( const e of getInclude(config, a) ){ if( result.includes(e) ) continue;
のようにincludesを使って避けて再帰的に検索することによりすべてのincludeファイルをひっぱてきている(環境変数とか見てincludeされる全ディレクトリを探すだけの気力はなかった)。
後はこれを
const fs=require('fs');
function isNew(file, target){
if( !fs.existsSync(file) ) return false;
if( typeof target==='string' ){
if( !fs.existsSync(target) ) return false;
return fs.statSync(file).mtimeMs>fs.statSync(target).mtimeMs;
}
else if( Array.isArray(target) ){
let time=0;
for( const a of target ){
if( fs.statSync(a).mtimeMs> time ) time=fs.statSync(a).mtimeMs;
}
// new_target=target.reduce((a, b)=>{
// console.log( fs.statSync(a).mtimeMs, fs.statSync(b).mtimeMs);
// if( fs.statSync(a).mtimeMs>fs.statSync(b).mtimeMs ) return a;
// else return b;
// });
return fs.statSync(file).mtimeMs>time
}
}
module.exports=isNew;
に引き渡してfileがtargetよりも新しいか聞く。stat関数の返り値を使っているがこれはファイル情報がObject形式になっており、
Stats {
dev: 2049,
mode: 33277,
nlink: 1,
uid: 1000,
gid: 1000,
rdev: 0,
blksize: 4096,
ino: 49022057,
size: 2057,
blocks: 8,
atimeMs: 1507794173159.5984,
mtimeMs: 1507793370555.0452,
ctimeMs: 1507793370619.049,
birthtimeMs: 1507793370619.049,
atime: 2017-10-12T07:42:53.160Z,
mtime: 2017-10-12T07:29:30.555Z,
ctime: 2017-10-12T07:29:30.619Z,
birthtime: 2017-10-12T07:29:30.619Z }
このような形式になる。MsはJavaScriptが世界協定時を使っているのでそのミリ秒だと思われる。atimeがアクセス時間、mtimeが修正時間、ctimeが作成時間なのでmtimeMsを取ってきて比較する(ctimeとbirthtimeはおんなじなのに作る必要があるのかな?バックアップとかに関係するのかな?)。
後はreduceを使った最大値の取得を使った、reduceを使うと例外処理などがめんどくさくなったので普通にループで回した。
そして実際にコンパイルしている部分をcompile/cpp.js
にモジュールとして切り出した。
const path=require('path');
const isNew=require('../isNew.js');
const child_process=require('child_process');
const getInclude=require('./getInclude.js');
const cpp={
obj: (config, out, input)=>{
const include_list=getInclude(config, input);
include_list.push(input);
if( isNew(out, include_list) ){
// console.log("cpp compile : ", out, 'is newer than ', input, ' skip');
return;
}
console.log("cpp compile", input, '->', out);
let cmd=config.compiler+' '+getFlags(config)+' -c -o '+out+' '+input;
// console.log('src : ', src, ' out : ',out);
try{ child_process.execSync(cmd); }
catch(e){
console.log('compile ', input, ' to', out, ' error');
throw new Error('c++ compile error');
}
},
exec: (config, out, input)=>{
if( Array.isArray(input) ){
console.log(" make exe file : ", out);
let cmd=config.compiler+' '+getFlags(config)+' -o '+out+' ';
for( const o of input ) cmd+=path.relative(process.cwd(), o)+' ';
cmd+=getLibs(config);
try{ child_process.execSync(cmd); }
catch(e){
console.log('make ', input, ' error');
throw new Error('c++ compile error');
}
}
}
}
今回の機能
とりあえずオブジェクトファイルはinclude_dir
で指定された範囲で依存関係を調べて再コンパイルが発生しないようにした。
makeをつかったdependは自分のmake depend
の書き方が悪いのか何故か更新してもコンパイルが発生しなかったりしたが今回はユーザーの入力に関係なく依存関係を調べているので作り方が悪いとかいうことはないはずである。
ソースファイルを直接調べたりしたのでmain関数を持つものだけリストアップしてリンカ時にそれだけ切り分けるとかできるはずだがまあ、ユーザーに分けてもらうほうが判定もしやすいしmain部分を別のディレクトリに分けておくのは良いと思う(そこにオブジェクトファイルを作ってしまうのは回避したほうが良いかもしれないが)。
とりあえず、まだ機能は足りないかもしれないが一応使えるレベルになったと思う。