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

# 仕様変更
前回は設定ファイルを引数で受け取っていたが`make.config.js`に固定した。
代わりに引数で動きを決められるようにした、単にcleanをしたかっただけだけど

# 実装
```js:jsmakecpp
#!/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設定を取ってくるところは

```js:getFlags.js
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`できるがその場合、更新チェックはされない。外部ライブラリなどはこっちで渡したほうがチェックた少なくなる。

```js:getInclude.js
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される全ディレクトリを探すだけの気力はなかった）。

後はこれを

```js:isNew.js                                                                                                
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を使うと例外処理などがめんどくさくなった](https://qiita.com/kentaro1530/items/29b07d701c4672bcea69)ので普通にループで回した。

そして実際にコンパイルしている部分を`compile/cpp.js`にモジュールとして切り出した。

```js: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部分を別のディレクトリに分けておくのは良いと思う（そこにオブジェクトファイルを作ってしまうのは回避したほうが良いかもしれないが）。
とりあえず、まだ機能は足りないかもしれないが一応使えるレベルになったと思う。