VB6にもmakeを。

## まえがき

VB6の[IDE](https://ja.wikipedia.org/wiki/%E7%B5%B1%E5%90%88%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83)はmake的挙動をやってくれない。つまり一連のソースコードになんの変更が無くてもコンパイルを実行すると新しいバイナリファイルが生成される。これだけならまだいいのだがコンパイルするたびに異なるバイナリファイルが生成される。つまり、２つのバイナリファイルが機能的に同一かそれとも何か変更されているかの区別が付かない。これが第一の問題。

もう一つ、よくある構成で

```Console
\---Proj
  +---Common
  |     CommonModule.bas
  |
  +---PreExe
  |     PreExe.vbp ←CommonModule.bas参照している
  |
  +---MainExe
  |     MainExe.vbp ←CommonModule.bas参照している
  |
  \---PostExe
        PostExe.vbp ←CommonModule.bas参照していない
```

こんな感じに物理ファイルレベルでソースコードを共有することは良くある。ここでCommonフォルダ内のCommonModule.basのあるメソッドを変更したとしよう。変更したからコンパイルする必要があるのはどれだろう。必要が無いのはどれだろう。片っ端から漏れなくコンパイルしても良いが必要が無いのにコンパイルすると第一の問題のように機能的変更は何もないのに異なるバイナリが生成されてしまう。

## make的挙動

[make](https://ja.wikipedia.org/wiki/Make)

本家makeコマンドを使っても勿論実現できるとは思うがここではPython3でvb専用のmake的挙動を行うプログラムを書いてみる(makeなんて40年以上前からあるのにどうしてVB6の開発環境に載せんかったのか・・・)

VBのプロジェクトファイル(.vbp)を参照し下記条件のいずれかを満たす場合コンパイルを実行する。

- 実行ファイルが存在しない
- 実行ファイルよりプロジェクトファイルが新しい(例えばバージョンだけ変更したとか)
- 実行ファイルより新しいソースファイルがある

## 開発環境

- Python : 3.8.3
- Visual Studio Code : 1.49.2

## ソースコード

プロジェクトファイル(.vbp)を扱うクラス

```Python:projectfile.py
import child
import os

class ProjectFile:
    __lines = []
    __exe_name32 = ""
    __children = []

    def get_exe_name32(self):
        line = [i for i in self.__lines if i.startswith("ExeName32=")]

        if len(line) != 0:
            (_, value) = line[0].split('=')
            self.exe_name32 = value.replace('\"', '').replace('\n', '')

    def get_children(self):
        keys = ["Clas", "Form", "Modu"]

        servived_lines = [i for i in self.__lines if i[0:4] in keys]

        for line in servived_lines:
            c = child.Child(line, os.path.dirname(self.fullpath))
            self.__children.append(c)

    @property
    def exe_name32(self):
        return self.__exe_name32

    @exe_name32.setter
    def exe_name32(self, value):
        self.__exe_name32 = value

    @property
    def children(self):
        return self.__children

    def __init__(self, fullpath):
        self.fullpath = fullpath
        with open(fullpath, mode='r') as f:
            self.__lines = f.readlines()
        self.get_exe_name32()
        self.get_children()
```

プロジェクトファイルに記載されているソースファイル(.bas, .cls, .frm)を扱うクラス

```Python:child.py
import os
import datetime

class Child:
    __full_path = ""
    __last_write_time = ""

    @property
    def full_path(self):
        return self.__full_path

    @property
    def last_write_time(self):
        return self.__last_write_time

    def __init__(self, line, basepath):
        (_, value) = line.split('=')

        if (';' in value):
            (_, item) = value.split(';')
            filename = item.strip()
        else:
            filename = value.strip()

        self.__full_path = os.path.join(basepath, filename)
        self.__last_write_time = os.path.getmtime(self.__full_path)
```

本体

```Python:vbmake.py
import projectfile
import sys
import os
import datetime
import subprocess

if __name__ == "__main__":
    if (len(sys.argv) != 2):
        print("vbmake.py vbpfullpath")
        x = input()
        exit

    vbp_full_path = sys.argv[1]
    vbp = projectfile.ProjectFile(vbp_full_path)
    exe_full_path = os.path.join(os.path.dirname(vbp_full_path), vbp.exe_name32)

    if os.path.exists(exe_full_path):
        standard_time_stamp = os.path.getmtime(exe_full_path)
        print(f"TimeStamp={datetime.datetime.fromtimestamp(standard_time_stamp)}")

        exe_not_found = False
        is_new_vbp = os.path.getmtime(vbp_full_path) > standard_time_stamp
        are_new_sources = any([i.last_write_time > standard_time_stamp for i in vbp.children])
    else:
        exe_not_found = True

    # 1)実行ファイルが存在しない
    # 2)VBPファイルが実行ファイルより新しい
    # 3)実行ファイルより新しいソースファイルがある
    if exe_not_found or is_new_vbp or are_new_sources:
        compiler1 = "C:\\Program Files\\Microsoft Visual Studio\\VB98\\VB6.EXE"
        compiler2 = "C:\\Program Files (x86)\\Microsoft Visual Studio\\VB98\\VB6.EXE"
        option = "/make"

        if os.path.exists(compiler1):
            compiler = compiler1
        else:
            compiler = compiler2

        print(f"{compiler} {option} {vbp_full_path}")
        subprocess.run([compiler, option, vbp_full_path])
    else:
        print("コンパイル不要")
```

コンパイラのフルパスが２つあるのはOSやx86/X64でVB6のインストール先が異なるので。確かcompiler1の方がW10(x64)でcompiler2の方がW7(x64)だったような。

## おまけ

いつもはバッチファイルを使って

```Console:make.bat
py vbmake.py %1
```

みたいなことをするのだが、[pyinstaller](https://pypi.org/project/pyinstaller/)なるものを知ったのでexeに仕立て上げてみる。Python入ってない環境でも動かすことができるようになるのはグレイト。

```Console
>pip install pyinstaller
```

して

```Console
>pyinstaller vbmake.py
```

以上。案の定10MB超のvbmake.exeが生成された(そらそうだよね)