川獺の外部記憶

なんでも残しておく闇鍋みたいな備忘録

Windowsのmsiパッケージを編集する

最近、Visual Studio Installer Projects でmsiを作る手法をよく使います。

この手法は非常に簡単な一方、msi本来の機能の一部しか活かせません。

基本的にはInstaller Projectsの容易さを享受しつつ、どうしても必要になったところだけマニュアル操作で組み替えられないか色々試してみた結果、Pythonの標準ライブラリ「msilib」を用いて弄る方法がしっくり来たので紹介します。*1

なかなかこんなことする人が居ないからか、情報がほとんどなくて少し辛かったです。

準備

環境はWindowsを想定。Python 3.xorca をインストールしておきます。また、何らかの方法でmsiを作れる環境を用意しておきます。(個人だとVS CommunityとInstaller Projectsの組み合わせが気楽です。)

orcaWindows SDKに付属しているMicrosoft公式のmsi編集ツールで、msiの中身をざっと見るのに使います。

msiを作成する

適当にmsiを作成します。

今回は「sample01.txt」と「sample02.txt」を「C:\sampleInstalled\conf\」にインストールするmsiを作りました。

今回のテーマ

今回の主題として、インストーラを適用する前の当該のフォルダが以下のようになっている場合を考えます。

C:\SAMPLEINSTALLED
+---conf
|       sample01.txt
|       sample02.txt
|       sample03.txt
|       sample04.txt
|
\---conf.default
        sample01.txt
        sample02.txt
        sample03.txt
        sample04.txt

ここで、インストールするファイルは「sample01.txt」と「sample02.txt」のみですが、「conf」フォルダ下のそれ以外の「sample*.txt」を全て削除したあとでインストールを行いたいものとします。

作ったmsiorcaで覗いてみる

作ったmsiを覗いてみると、以下のような「RemoveFile」テーブルが存在することがわかります。 f:id:marineotter:20200617231356p:plain

なんとなく、ここを弄ればいい感じになりそうな気がするので、適当に下図のように編集して保存(msiのDBにコミット)してみます。 f:id:marineotter:20200617231612p:plain

…うまく動きました。ここまでで試作はOKです。

C:\SAMPLEINSTALLED
+---conf
|       sample01.txt
|       sample02.txt
|
\---conf.default
        sample01.txt
        sample02.txt
        sample03.txt
        sample04.txt

ちなみに適当に弄った内容ですが、下記公式ドキュメントを参考にしました。

RemoveFile Table - Win32 apps | Microsoft Docs

コーヒーブレイク:「ここまでで目標達成してるのでは?」に対して

ここまでで、「私という人間が居れば、この作業ができる」という技術調査ができました。 この調査までを「技術」だという人も結構居ますが、私はここから属人性を排除して万人が再現できる手順を作成するところが技術者としての腕の見せ所だと考えています。

その手段は色々あり、手順書の作成や自動化プログラムの作成などがありますが、今回はorcaへの依存性も排除するために、msi DBを直接弄るAPIを叩く方法まで一気に習得します。

ただし、Win32APIの習熟はぶっちゃけめんどくさいので、Pythonライブラリに頼ります。

Pythonライブラリがなくなったら諦めてWin32APIを叩きます。*2

ところで、このあたりで力尽きてきたので、ここから先はすみませんがソースコードを読んで察して下さい。

OrcaでやったことをPythonで固定化する。

出来上がったコードがこちらになります。

import os
import msilib

os.chdir(os.path.dirname(os.path.abspath(__file__)))

db = msilib.OpenDatabase("Release/Setup1.msi", msilib.MSIDBOPEN_TRANSACT)

view = db.OpenView("SELECT * FROM Directory WHERE DefaultDir='CONF|conf'")
view.Execute(None)
rec = view.Fetch()
tgtdir = rec.GetString(1)
view.Close()

view = db.OpenView("SELECT * FROM Component WHERE Directory_='TARGETDIR'")
view.Execute(None)
rec = view.Fetch()
tgtcomponent = rec.GetString(1)
view.Close()

msilib.add_data(db, "RemoveFile", [
    ("000001", tgtcomponent, "sample*.txt", tgtdir, 1)
])

db.Commit()
db.Close()

このコードはVisual Studio Installer Projectsのカレントディレクトリ(slnと同じディレクトリ)において、Releaseディレクトリ下に生成される「Setup1.msi」に対して処理することのみを前提としているなど、色々アレな点があります。

著者はそのあたりの手順の固定化は慣れているため、今回の技術勉強では敢えて手を抜いている点にご留意下さい。あんまり真似しないで下さい。今回真面目に書いたのはあくまでWin32APIラッパーとしてのmsilib使用部分だけ。*3

実運用への課題

実運用では、Installer Projectsのソリューションビルドと、このスクリプトのlaunchの両方が必要になることを忘れないようにする必要がある。

そのため、ビルドスクリプトなどを上に被せてやり、ビルド方法の周知(と言っても「これを叩け」というだけですが)をする必要はあります。

そこまでCIの思想で自動化できる環境があれば、自動化してしまうのが幸せなのは言うまでもないですが。

あとがき

もう学生じゃないので、この速度で不慣れな技術を習得するのはなかなかアレ。歳は取りたくないもんですわ…

*1:Pythonにこだわったわけではなく、目的から色々試行錯誤した結果、なぜかPythonに帰ってきました。なんでこんな標準ライブラリまで用意されてるんでしょうね… 驚きました。

*2:まあVBSでも書けるんですが…私アレ嫌い。

*3:っていって逃げておく。普段マサカリ飛ばしてる分自分に返ってくると怖い。