Inno Setupに新設されたFile Version functions解説


## File Version functions
Inno SetupのV6.1からFile Version functionsというカテゴリの関数が追加されました。What's Newの文章から引用します。

> Added new GetPackedVersion, PackVersionNumbers, PackVersionComponents, ComparePackedVersion, SamePackedVersion, UnpackVersionNumbers, UnpackVersionComponents, and VersionToStr support functions.

[ヘルプにある関数のリファレンス](https://jrsoftware.org/ishelp/topic_scriptfunctions.htm)を見ると説明がほとんどないので使い方を調べてみました。

## ファイルのバージョニング・ルール

マイクロソフトによるファイルのバージョニング・ルールを知っていると、これら関数への理解が早いです。

**VERSIONINFO resource**
https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource

一般にファイルのバージョンは、`a.b.c.d`のような4つの数値の並びで示されますが、各数値は16bit整数で表されます。ここから各数値の範囲は0～65535であることが分かります。すなわちバージョンとして表現できるのは、`0.0.0.0`～`65535.65535.65535.65535`の範囲、ということになります。また上記のドキュメントを読むと、32bit整数を２つ使って表記する方式もあることが分かります。これを踏まえて、関数名に使われている単語の意味を定義します。

|単語|意味|
|:--|:--|
|VersionComponents|16bit整数4つの形式|
|VersionNumbers|32bit整数2つの形式|
|PackedVersion|64bit整数1つの形式|
|Get|ファイルからバージョンを取得する|
|Compare|2つのバージョンを比較して、大小関係を取得する|
|same|2つのバージョンを比較して、同じかどうかを取得する|
|Pack|PackedVersionに変換する|
|Unpack|PackedVersionから別の形式に変換する|

ここまで分かってしまえば、各関数の使い方も分かってきます。また、ファイルバージョンは64bitあれば表現できるのだから、64bit整数に変換してバージョン比較できる、というロジックも見えてきます。

## EXE/DLLファイルとMSIファイルのバージョニング・ルールは異なる

誤解してはいけないのは、EXE/DLLファイルとMSIファイルのバージョニング・ルールは異なる、ということです。MSIファイルのバージョニングは、以下に解説があります。

**ProductVersion property**
https://docs.microsoft.com/en-us/windows/win32/msi/productversion

基本的に3つの数値を「.」で区切って並べますが、4つの数値を使うこともできます。しかし、4番目の数値が付いていても、Windows Installerはこの数値を無視します。使える番号の範囲も異なります。バージョンとして使える範囲は`0.0.0`～`255.255.65535`となります。

とはいえ、[ソース](https://github.com/jrsoftware/issrc/blob/main/Projects/ScriptFunc_R.pas#L1497)を見れば分かるように、新設された関数は単純に数値演算しているだけなので、MSIファイルのバージョン比較にも使えそうです。

## 実際に使ってみる

実験用に下記の3つのEXEファイルを作り、インストーラーと同じディレクトリに置きます。

|ファイル名|ファイルバージョン|
|:--|:--|
|f8_4_2_1.exe|8.4.2.1|
|f1_1_1_1.exe|1.1.1.1|
|f65535_65535_65535_65535.exe|65535.65535.65535.65535|

各関数の動作を確認します。各関数のエラーをチェックしていないので、実際に使用するときは適宜追加してください。

### GetVersionNumbersString()

```pascal
var
  FilenameA, VersionA: String;
begin
  FilenameA := ExpandConstant('{src}\f8_4_2_1.exe');
  GetVersionNumbersString(FilenameA, VersionA);
  MsgBox('GetVersionNumbersString(FilenameA, VersionA)'+#13#10+#13#10
    +'VersionA : '+VersionA
    , mbInformation, MB_OK);
end;
```

![GetVersionNumbersString.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/b573d0c2-881b-4e69-9a91-02a04dbe2ea0.png)

### GetVersionComponents()

```pascal
var
  FilenameA: String;
  MajorA, MinorA, RevisionA, BuildA: Word;
begin
  FilenameA := ExpandConstant('{src}\f8_4_2_1.exe');
  GetVersionComponents(FilenameA, MajorA, MinorA, RevisionA, BuildA);
  MsgBox('GetVersionComponents(FilenameA, MajorA, MinorA, RevisionA, BuildA)'+#13#10+#13#10
    +'MajorA : '+IntToStr(MajorA)+#13#10
    +'MinorA : '+IntToStr(MinorA)+#13#10
    +'RevisionA : '+IntToStr(RevisionA)+#13#10
    +'BuildA : '+IntToStr(BuildA)
    , mbInformation, MB_OK);
end;
```

![GetVersionComponents.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/e9130e32-9823-6ae6-f6a9-58e617edef39.png)

### GetVersionNumbers()

```pascal
var
  FilenameA: String;
  VersionMSA, VersionLSA: Cardinal;
begin
  FilenameA := ExpandConstant('{src}\f8_4_2_1.exe');
  GetVersionNumbers(FilenameA, VersionMSA, VersionLSA);
  MsgBox('GetVersionNumbers(FilenameA, VersionMSA, VersionLSA)'+#13#10+#13#10
    +'VersionMSA : '+IntToStr(VersionMSA)+#13#10
    +'VersionLSA : '+IntToStr(VersionLSA)
    , mbInformation, MB_OK);
end;
```

![GetVersionNumbers.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/6b70abd2-a65a-8da2-d6af-6d554efeac67.png)

このような数字になるのは、以下の計算がされているからです。

```math
\begin{align}
VersionMSA&=MajorA * 2^{16} + MinorA\\
&=8 * 2^{16} + 4\\
&=524292
\end{align}
```

```math
\begin{align}
VersionLSA&=RevisionA * 2^{16} + BuildA\\
&=2 * 2^{16} + 1\\
&=131073
\end{align}
```

検算してみます[^1]。

```pascal
var
  Major32, Minor32, Revision32, Build32: Cardinal;
begin
  Major32 := 8;
  Minor32 := 4;
  Revision32 := 2;
  Build32 := 1;
  MsgBox('Verify VersionNumbers'+#13#10+#13#10
    +'(MajorA shl 16)+MinorA : '+IntToStr((Major32 shl 16)+Minor32)+#13#10
    +'(RevisionA shl 16)+BuildA : '+IntToStr((Revision32 shl 16)+Build32)+#13#10
    , mbInformation, MB_OK);
end;
```

![VerifyVersionNumbers.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/296a4f46-4621-69e6-5e10-f2540d2f33f1.png)

### GetPackedVersion()

```pascal
var
  FilenameA: String;
  VersionA64: Int64;
begin
  FilenameA := ExpandConstant('{src}\f8_4_2_1.exe');
  GetPackedVersion(FilenameA, VersionA64);
  MsgBox('GetPackedVersion(FilenameA, VersionA64)'+#13#10+#13#10
    +'VersionA64 : '+IntToStr(VersionA64)
    , mbInformation, MB_OK);
end;
```

![GetPackedVersion.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/4447548b-17e3-537b-fa9b-91de0097ac35.png)

このような数字になるのは、以下の計算がされているからです。

```math
\begin{align}
VersionA64&=MajorA * 2^{48} + MinorA * 2^{32} + RevisionA * 2^{16} + BuildA\\
&=8 * 2^{48} + 4 * 2^{32} + 2 * 2^{16} + 1\\
&=2251816993685505
\end{align}
```

検算してみます[^1]。

```pascal
var
  Major64, Minor64, Revision64, Build64: Int64;
begin
  Major64 := 8;
  Minor64 := 4;
  Revision64 := 2;
  Build64 := 1;
  MsgBox('Verify PackedVersion'+#13#10+#13#10
    +'(MajorA shl 48)+(MinorA shl 32)+(RevisionA shl 16)+BuildA : '
    +IntToStr((Major64 shl 48)+(Minor64 shl 32)+(Revision64 shl 16)+Build64)
    , mbInformation, MB_OK);
end;
```

![VerifyPackedVersion.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/bf66a4f9-24f6-b15d-05a8-dbb3c9300a38.png)

### ComparePackedVersion()

```pascal
var
  FilenameA, FilenameB, FilenameC, VersionA, VersionB, VersionC: String;
  VersionA64, VersionB64, VersionC64: Int64;
begin
  FilenameA := ExpandConstant('{src}\f8_4_2_1.exe');
  FilenameB := ExpandConstant('{src}\f1_1_1_1.exe');
  FilenameC := ExpandConstant('{src}\f65535_65535_65535_65535.exe');
  GetVersionNumbersString(FilenameA, VersionA);
  GetVersionNumbersString(FilenameB, VersionB);
  GetVersionNumbersString(FilenameC, VersionC);
  GetPackedVersion(FilenameA, VersionA64);
  GetPackedVersion(FilenameB, VersionB64);
  GetPackedVersion(FilenameC, VersionC64);
  MsgBox('VersionA : '+VersionA+#13#10
    +'VersionB : '+VersionB+#13#10
    +'VersionC : '+VersionC+#13#10+#13#10
    +'ComparePackedVersion(VersionA64, VersionB64)'+#13#10
    +IntToStr(ComparePackedVersion(VersionA64, VersionB64))+#13#10+#13#10
    +'ComparePackedVersion(VersionA64, VersionC64)'+#13#10
    +IntToStr(ComparePackedVersion(VersionA64, VersionC64))+#13#10+#13#10
    +'ComparePackedVersion(VersionA64, VersionA64)'+#13#10
    +IntToStr(ComparePackedVersion(VersionA64, VersionA64))
    , mbInformation, MB_OK);
end;
```

![ComparePackedVersion.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/27c26bf8-b0b4-7485-ed3d-68fb6f167f32.png)

### SamePackedVersion

```pascal
var
  FilenameA, FilenameB, CompResultA, CompResultB: String;
  VersionA64, VersionB64: Int64;
begin
  FilenameA := ExpandConstant('{src}\f8_4_2_1.exe');
  FilenameB := ExpandConstant('{src}\f1_1_1_1.exe');
  GetPackedVersion(FilenameA, VersionA64);
  GetPackedVersion(FilenameB, VersionB64);
  if SamePackedVersion(VersionA64, VersionB64) then begin
    CompResultA := 'True';
  end else begin
    CompResultA := 'False';
  end;
  if SamePackedVersion(VersionA64, VersionA64) then begin
    CompResultB := 'True';
  end else begin
    CompResultB := 'False';
  end;
  MsgBox('VersionA : '+VersionA+#13#10
    +'VersionB : '+VersionB+#13#10+#13#10
    +'SamePackedVersion(VersionA64, VersionB64)'+#13#10
    +CompResultA+#13#10+#13#10
    +'SamePackedVersion(VersionA64, VersionA64)'+#13#10
    +CompResultB+#13#10
    , mbInformation, MB_OK);
end;
```

![SamePackedVersion.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/3c63a6b4-f979-ceb9-0db8-b3c94012601e.png)

### PackVersionComponents()/PackVersionNumbers()

```pascal
var
  FilenameA: String;
  MajorA, MinorA, RevisionA, BuildA: Word;
  VersionMSA, VersionLSA: Cardinal;
begin
  FilenameA := ExpandConstant('{src}\f8_4_2_1.exe');
  GetVersionComponents(FilenameA, MajorA, MinorA, RevisionA, BuildA);
  GetVersionNumbers(FilenameA, VersionMSA, VersionLSA);
  MsgBox('PackVersionComponents(MajorA, MinorA, RevisionA, BuildA)'+#13#10
    +IntToStr(PackVersionComponents(MajorA, MinorA, RevisionA, BuildA))+#13#10+#13#10
    +'PackVersionNumbers(VersionMSA, VersionLSA)'+#13#10
    +IntToStr(PackVersionNumbers(VersionMSA, VersionLSA))
    , mbInformation, MB_OK);
end;
```

![PackVersionNumbers_PackVersionComponents.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/fdd4ecc2-a3e2-22a3-2a7f-1d9f3e0677d7.png)

### UnpackVersionComponents()/UnpackVersionNumbers()

```pascal
var
  FilenameA: String;
  MajorB, MinorB, RevisionB, BuildB: Word;
  VersionMSB, VersionLSB: Cardinal;
  VersionA64: Int64;
begin
  FilenameA := ExpandConstant('{src}\f8_4_2_1.exe');
  GetPackedVersion(FilenameA, VersionA64);
  UnpackVersionComponents(VersionA64, MajorB, MinorB, RevisionB, BuildB);
  UnpackVersionNumbers(VersionA64, VersionMSB, VersionLSB);
  MsgBox('VersionA64 : '+IntToStr(VersionA64)+#13#10+#13#10
    +'UnpackVersionComponents(VersionA64, MajorB, MinorB, RevisionB, BuildB)'+#13#10
    +'MajorB : '+IntToStr(MajorB)+#13#10
    +'MinorB : '+IntToStr(MinorB)+#13#10
    +'RevisionB : '+IntToStr(RevisionB)+#13#10
    +'BuildB : '+IntToStr(BuildB)+#13#10+#13#10
    +'UnpackVersionNumbers(VersionA64, VersionMSB, VersionLSB)'+#13#10
    +'VersionMSB : '+IntToStr(VersionMSB)+#13#10
    +'VersionLSB : '+IntToStr(VersionLSB)
    , mbInformation, MB_OK);
end;
```

![UnpackVersionComponents_UnpackVersionNumbers.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/5fa44df8-f346-4324-9086-e0b3052537ab.png)

### VersionToStr()

```pascal
var
  FilenameA: String;
  VersionA64: Int64;
begin
  FilenameA := ExpandConstant('{src}\f8_4_2_1.exe');
  GetPackedVersion(FilenameA, VersionA64);
  MsgBox('VersionA64 : '+IntToStr(VersionA64)+#13#10+#13#10
    +'VersionToStr(VersionA64)'+#13#10
    +VersionToStr(VersionA64)
    , mbInformation, MB_OK);
end;
```

![VersionToStr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/445a1de6-98e1-1b75-8719-16f91b477485.png)

## 実験用にEXEファイルにバージョンを入れる

もちろんVisual Studioを使ったやり方もありますが、私はリソースコンパイラを使った方法がシンプルなのでよく使います。

先ほど紹介した[VERSIONINFO resource](https://docs.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource)の下の方にリソースファイルの例があるので、マネして書いていきます。

```rc:f8_4_2_1.rc
#include <winver.h>

#define VER_FILEVERSION             8,4,2,1
#define VER_FILEVERSION_STR         "8.4.2.1\0"
#define VER_PRODUCTVERSION          8,4,2,1
#define VER_PRODUCTVERSION_STR      "8.4.2\0"

VS_VERSION_INFO    VERSIONINFO 
FILEVERSION        VER_FILEVERSION
PRODUCTVERSION     VER_PRODUCTVERSION
FILEFLAGSMASK      VS_FFI_FILEFLAGSMASK 
FILEFLAGS          0x00000000L
FILEOS             VOS__WINDOWS32
FILETYPE           VFT_APP
FILESUBTYPE        VFT2_UNKNOWN
BEGIN
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x0409, 1252
    END

    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "040904E4"
        BEGIN
            VALUE "CompanyName",      "tohshima"
            VALUE "FileDescription",  "for File Version Test"
            VALUE "FileVersion",      VER_FILEVERSION_STR
            VALUE "InternalName",     "<InternalName>\0"
            VALUE "LegalCopyright",   "Copyright (C) 2021 tohshima.\0"
            VALUE "OriginalFilename", "fx_x_x_x.exe\0"
            VALUE "ProductName",      "fx_x_x_x\0"
            VALUE "ProductVersion",   VER_PRODUCTVERSION_STR
        END 
    END
END
```

`x86 Native Tools Command Prompt`を起動して、リソースコンパイラで`.res`ファイルに変換します。

```batch
rc f8_4_2_1.rc
```

これで、`f8_4_2_1.res`が得られます。適当にCのプログラムを作ります。

```c:f8_4_2_1.c
int main(){
	return 0;
}
```

コンパイルします。

```batch
cl f8_4_2_1.c f8_4_2_1.res
```

これで、バージョン`8.4.2.1`の`f8_4_2_1.exe`が得られます。

## 実験用のインストーラーを作る

今回の実験で使用したインストーラーのソースを以下に示します。`Uninstallable=no`とすることで実行後にテスト環境に何も残さないようにできます。`DisableReadyPage=True`を有効にするために、`InitializeWizard()`の最後で`WizardForm.Show();`を実行します。今回は管理者権限で実行する必要がないので、User Account Controlダイアログの表示を抑止するために`PrivilegesRequired=lowest`とします。

```pascal:testFileVersionFunctions.iss
[Setup]
DisableReadyPage=True
DisableReadyMemo=True
DisableFinishedPage=True
AppName=testFileVersionFunctions
CreateAppDir=False
Uninstallable=no
AppVersion=1.0.0
PrivilegesRequired=lowest
OutputDir=.

[Code]
procedure InitializeWizard();
var
  FilenameA, FilenameB, FilenameC, VersionA, VersionB, VersionC, CompResultA, CompResultB: String;
  MajorA, MinorA, RevisionA, BuildA, MajorB, MinorB, RevisionB, BuildB: Word;
  VersionMSA, VersionLSA, VersionMSB, VersionLSB, Major32, Minor32, Revision32, Build32: Cardinal;
  VersionA64, VersionB64, VersionC64, Major64, Minor64, Revision64, Build64: Int64;
begin
  FilenameA := ExpandConstant('{src}\f8_4_2_1.exe');
  FilenameB := ExpandConstant('{src}\f1_1_1_1.exe');
  FilenameC := ExpandConstant('{src}\f65535_65535_65535_65535.exe');
  //
  GetVersionNumbersString(FilenameA, VersionA);
  MsgBox('GetVersionNumbersString(FilenameA, VersionA)'+#13#10+#13#10
    +'VersionA : '+VersionA
    , mbInformation, MB_OK);
  //
  GetVersionComponents(FilenameA, MajorA, MinorA, RevisionA, BuildA);
  MsgBox('GetVersionComponents(FilenameA, MajorA, MinorA, RevisionA, BuildA)'+#13#10+#13#10
    +'MajorA : '+IntToStr(MajorA)+#13#10
    +'MinorA : '+IntToStr(MinorA)+#13#10
    +'RevisionA : '+IntToStr(RevisionA)+#13#10
    +'BuildA : '+IntToStr(BuildA)
    , mbInformation, MB_OK);
  //
  GetVersionNumbers(FilenameA, VersionMSA, VersionLSA);
  MsgBox('GetVersionNumbers(FilenameA, VersionMSA, VersionLSA)'+#13#10+#13#10
    +'VersionMSA : '+IntToStr(VersionMSA)+#13#10
    +'VersionLSA : '+IntToStr(VersionLSA)
    , mbInformation, MB_OK);
  //
  Major32 := MajorA;
  Minor32 := MinorA;
  Revision32 := RevisionA;
  Build32 := BuildA;
  MsgBox('Verify VersionNumbers'+#13#10+#13#10
    +'(MajorA shl 16)+MinorA : '+IntToStr((Major32 shl 16)+Minor32)+#13#10
    +'(RevisionA shl 16)+BuildA : '+IntToStr((Revision32 shl 16)+Build32)+#13#10
    , mbInformation, MB_OK);
  //
  GetPackedVersion(FilenameA, VersionA64);
  MsgBox('GetPackedVersion(FilenameA, VersionA64)'+#13#10+#13#10
    +'VersionA64 : '+IntToStr(VersionA64)
    , mbInformation, MB_OK);
  //
  Major64 := MajorA;
  Minor64 := MinorA;
  Revision64 := RevisionA;
  Build64 := BuildA;
  MsgBox('Verify PackedVersion'+#13#10+#13#10
    +'(MajorA shl 48)+(MinorA shl 32)+(RevisionA shl 16)+BuildA : '
    +IntToStr((Major64 shl 48)+(Minor64 shl 32)+(Revision64 shl 16)+Build64)
    , mbInformation, MB_OK);
  //
  GetVersionNumbersString(FilenameB, VersionB);
  GetVersionNumbersString(FilenameC, VersionC);
  GetPackedVersion(FilenameB, VersionB64);
  GetPackedVersion(FilenameC, VersionC64);
  MsgBox('VersionA : '+VersionA+#13#10
    +'VersionB : '+VersionB+#13#10
    +'VersionC : '+VersionC+#13#10+#13#10
    +'ComparePackedVersion(VersionA64, VersionB64)'+#13#10
    +IntToStr(ComparePackedVersion(VersionA64, VersionB64))+#13#10+#13#10
    +'ComparePackedVersion(VersionA64, VersionC64)'+#13#10
    +IntToStr(ComparePackedVersion(VersionA64, VersionC64))+#13#10+#13#10
    +'ComparePackedVersion(VersionA64, VersionA64)'+#13#10
    +IntToStr(ComparePackedVersion(VersionA64, VersionA64))
    , mbInformation, MB_OK);
  //
  if SamePackedVersion(VersionA64, VersionB64) then begin
    CompResultA := 'True';
  end else begin
    CompResultA := 'False';
  end;
  if SamePackedVersion(VersionA64, VersionA64) then begin
    CompResultB := 'True';
  end else begin
    CompResultB := 'False';
  end;
  MsgBox('VersionA : '+VersionA+#13#10
    +'VersionB : '+VersionB+#13#10+#13#10
    +'SamePackedVersion(VersionA64, VersionB64)'+#13#10
    +CompResultA+#13#10+#13#10
    +'SamePackedVersion(VersionA64, VersionA64)'+#13#10
    +CompResultB+#13#10
    , mbInformation, MB_OK);
  //
  MsgBox('PackVersionComponents(MajorA, MinorA, RevisionA, BuildA)'+#13#10
    +IntToStr(PackVersionComponents(MajorA, MinorA, RevisionA, BuildA))+#13#10+#13#10
    +'PackVersionNumbers(VersionMSA, VersionLSA)'+#13#10
    +IntToStr(PackVersionNumbers(VersionMSA, VersionLSA))
    , mbInformation, MB_OK);
  //
  UnpackVersionComponents(VersionA64, MajorB, MinorB, RevisionB, BuildB);
  UnpackVersionNumbers(VersionA64, VersionMSB, VersionLSB);
  MsgBox('VersionA64 : '+IntToStr(VersionA64)+#13#10+#13#10
    +'UnpackVersionComponents(VersionA64, MajorB, MinorB, RevisionB, BuildB)'+#13#10
    +'MajorB : '+IntToStr(MajorB)+#13#10
    +'MinorB : '+IntToStr(MinorB)+#13#10
    +'RevisionB : '+IntToStr(RevisionB)+#13#10
    +'BuildB : '+IntToStr(BuildB)+#13#10+#13#10
    +'UnpackVersionNumbers(VersionA64, VersionMSB, VersionLSB)'+#13#10
    +'VersionMSB : '+IntToStr(VersionMSB)+#13#10
    +'VersionLSB : '+IntToStr(VersionLSB)
    , mbInformation, MB_OK);
  //
  MsgBox('VersionA64 : '+IntToStr(VersionA64)+#13#10+#13#10
    +'VersionToStr(VersionA64)'+#13#10
    +VersionToStr(VersionA64)
    , mbInformation, MB_OK);
  //
  WizardForm.Show();  // wpReady画面を出さない
end;
```

[^1]: 数式上は2のべき乗を掛け合わせていますが、Pascalで計算する際には`shl`演算子（shift left）を使って計算しています。