【実務・中級編】MinGW-w64でクロスコンパイルした実行ファイルが依存するシステムDLLを自動抽出するシェルスクリプト術 – 実行環境・ランタイム・コンパイラ生産性向上バイブル

こんにちは。テックリードの私だ。

開発チームの席を歩いていると、いまだによくこんな悲鳴耳にしないか?
「Windowsで動かすためにMinGW-w64でクロスコンパイルしたexeを別環境のテスターに渡したら、『`libwinpthread-1.dll` が見つかりません』って怒られて起動しないんだけど!」

――おいおい、令和のこの時代に、手作業でDLLを探してexeと同じフォルダにポチポチコピーしているのか? そんな泥臭い作業をしているうちは、CI/CDの自動化も、スピーディーな納品も夢のまた夢だ。

今回は、MSYS2/MinGW-w64環境におけるクロスコンパイルの苦痛を根絶し、ビルド成果物と依存DLLを一撃でパッケージングする「至高の依存DLL自動抽出シェルスクリプト」をあなたに授けよう。ネットの海を漂う薄っぺらなコピペ記事とは一線を画す、PEヘッダの構造とランタイムの依存関係の真理に踏み込んだプロの技法を解説する。

—

なぜMinGW-w64のバイナリはDLL地獄に陥るのか?

Linuxの `ldd` や動的リンカの挙動に慣れたエンジニアほど、Windows(PEフォーマット)のDLL解決メカニズムで足をすくわれる。

MinGW-w64で静的リンク(`-static`)の指定をあえて外す、あるいはC++の例外処理モデル(SEHやSJLJ)やPthreadsなどのランタイムライブラリを使用する場合、コンパイルされたバイナリは外部のDLLへの参照(Import Table)を抱えた状態で生まれてくる。

Windowsのローダは、exeと同じディレクトリか、あるいはシステムの `PATH` が通ったディレクトリからしかDLLを探さない。そのため、開発マシンのMSYS2シェル(`/mingw64/bin` がパスに通っている状態)では何何事もなく動いていたバイナリも、クリーンなWindows環境に持っていった途端に盛大にクラッシュするのだ。

これを解決するために、我々はバイナリのインポートテーブルを解析し、依存しているDLLをMSYS2のsysrootから自動的に狩り出して配布用ディレクトリに集約する仕組みをコード化しなければならない。

—

現場で即採用できる!依存DLL自動抽出・パッケージングシェルスクリプト

以下のシェルスクリプトは、私が実際のクロスプラットフォーム開発プロジェクトのCI(GitHub ActionsやGitLab CI)およびローカルビルドスクリプトで運用しているものの核心部分を抽出・汎用化したものだ。

Bash環境(MSYS2/MinGW64)で実行することを前提としている。

!/usr/bin/env bash

エラー発生時に即座にスクリプトを中断(堅牢性の担保)
set -euo pipefail

==========================================
設定エリア
==========================================
対象となるビルド済みバイナリ(exe, dll等)のパス
TARGET_BINARY=”${1:-}”

配布用パッケージを作成する出力先ディレクトリ
OUTPUT_DIR=”${2:-./dist_package}”

使用しているMinGWのアーキテクチャに応じた環境プレフィックス(例: x86_64-w64-mingw32)
環境変数 $MSYSTEM_PREFIX があればそれを活用、なければフォールバック
MINGW_PREFIX=”${MSYSTEM_PREFIX:-/mingw64}”

==========================================
引数のバリデーション
==========================================
if [ -z “$TARGET_BINARY” ]; then
echo “[ERROR] ターゲットとなるバイナリを指定してください。”
echo “Usage: $0 [output-directory]”
exit 1
fi

if [ ! -f “$TARGET_BINARY” ]; then
echo “[ERROR] 指定されたファイル ‘${TARGET_BINARY}’ が存在しません。”
exit 1
fi

echo “==========================================”
echo ” Starting DLL Packaging for: ${TARGET_BINARY}”
echo ” Output Directory : ${OUTPUT_DIR}”
echo ” MinGW Prefix : ${MINGW_PREFIX}”
echo “==========================================”

配布用ディレクトリの初期化
rm -rf “${OUTPUT_DIR}”
mkdir -p “${OUTPUT_DIR}”

1. メインのバイナリを指定ディレクトリにコピー
cp -v “${TARGET_BINARY}” “${OUTPUT_DIR}/”

コピー後のファイル名を取得
BINARY_NAME=$(basename “${TARGET_BINARY}”)
COPIED_BINARY=”${OUTPUT_DIR}/${BINARY_NAME}”

2. 依存関係の再帰的解決とコピーを行うための関数
resolve_dependencies() {
local bin_path=”$1″

# objdump と awk、grep を駆使して PEヘッダーの 「DLL Name」 セクションを抽出
# ※ ldd はMSYS2環境によっては挙動が不安定なため、objdump -p を使用するのが最も確実
local deps
deps=$(x86_64-w64-mingw32-objdump -p “${bin_path}” | grep “DLL Name:” | awk ‘{print $3}’)

for dll in ${deps}; do
# 3. システム標準DLL(Windowsの核心ライブラリ)は除外する
# 大文字小文字を区別せずに判定するためlowercaseに変換
local dll_lower
dll_lower=$(echo “$dll” | tr ‘[:upper:]’ ‘[:lower:]’)

case “${dll_lower}” in
# Windows OSの基本API群(これらはターゲットPCに必ず存在するため同梱不要)
kernel32.dll | user32.dll | gdi32.dll | advapi32.dll | shell32.dll | \
ole32.dll | oleaut32.dll | msvcrt.dll | ws2_32.dll | crypt32.dll | \
imm32.dll | version.dll | winmm.dll | shlwapi.dll | ntdll.dll | rpcrt4.dll)
echo ” [SKIP] System DLL: ${dll}”
continue
;;
esac

# 4. 配布先ディレクトリにすでにコピー済みの場合は無限ループ・重複コピーを防ぐためスキップ
if [ -f “${OUTPUT_DIR}/${dll}” ]; then
continue
}

# 5. MinGWのsysroot内から該当DLLのフルパスを探索
local dll_path=””
if [ -f “${MINGW_PREFIX}/bin/${dll}” ]; then
dll_path=”${MINGW_PREFIX}/bin/${dll}”
elif [ -f “${MINGW_PREFIX}/lib/${dll}” ]; then
dll_path=”${MINGW_PREFIX}/lib/${dll}”
fi

if [ -n “${dll_path}” ]; then
echo ” [COPY] Found dependency: ${dll} -> from ${dll_path}”
cp -v “${dll_path}” “${OUTPUT_DIR}/”

# 6. 【重要】DLLがさらに別のDLLに依存しているケース(孫依存)を解決するため再帰呼び出し
resolve_dependencies “${dll_path}”
else
echo ” [WARN] Dependency ‘${dll}’ required by ‘${bin_path}’ could not be found in ${MINGW_PREFIX}!”
fi
done
}

依存関係解決の実行開始
echo “Analyzing dependencies…”
resolve_dependencies “${COPIED_BINARY}”

echo “==========================================”
echo ” Packaging completed successfully!”
echo ” All artifacts are stored in: ${OUTPUT_DIR}”
echo “==========================================”

このスクリプトが「神」である理由(アーキテクチャの解説)

1. `ldd` ではなく `objdump -p` を採用している点
MSYS2環境で `ldd` を実行すると、MSYS2自体のPOSIXエミュレーションレイヤー(`msys-2.0.dll`)のパスを引いてしまい、真のMinGW-w64依存関係を見失うことがある。PEヘッダを直接叩く `x86_64-w64-mingw32-objdump -p` を使うことで、純粋なWindows側からの視点でインポートされているDLLを正確に100%キャプチャできる。
2. システムDLLのブラックリスト方式
`kernel32.dll` や `ntdll.dll` などを同梱しようとすると、Windowsのセキュリティ機構に弾かれたり、バージョン競合の原因になる。これらを確実に除外するホワイト/ブラックリスト制御をスクリプト内に組み込んでいる。
3. 孫依存(Transitive Dependencies)の再帰的解決
例えば、自作の `app.exe` が `libfoo.dll` に依存しており、その `libfoo.dll` がさらに `libgcc_s_seh-1.dll` に依存している場合、単純な1段階の抽出では漏れが発生する。上記の関数 `resolve_dependencies` は、コピーしたDLLに対してもさらに `objdump` をかけ、依存の連鎖をくまなくハントする。

—

チーム開発を加速させる:MSYS2/MinGW環境の共有化ルールと設定

個人のローカル環境に依存しがちなMinGW-w64/MSYS2だが、チーム開発でこれをやると「俺の環境ではビルドできるのに、あいつの環境ではエラーになる」という地獄のデスマーチが始まる。これを防ぐための鉄則を共有しよう。

1. パッケージ管理のコード化(`PKGBUILD` または セットアップスクリプト)

開発メンバー全員が完全に同一バージョンのツールチェインとライブラリを使うため、必要なpacmanパッケージのリストをJSONやシェルスクリプトで固定化する。

`setup-mingw-env.sh` (環境構築自動化スクリプト)

!/usr/bin/env bash
set -e

echo “Updating MSYS2 core packages…”
pacman -Syu –noconfirm

echo “Installing standard MinGW-w64 toolchain and common libraries…”
pacman -S –needed –noconfirm \
mingw-w64-x86_64-toolchain \
mingw-w64-x86_64-cmake \
mingw-w64-x86_64-ninja \
mingw-w64-x86_64-openssl \
mingw-w64-x86_64-boost

echo “MSYS2 MinGW-w64 environment setup is complete!”

2. VS Code による開発エクスペリエンスの極限最適化

チームメンバーの多くが VS Code を使っているはずだ。MinGW環境でのクロスコンパイルとデバッグをシームレスに行うための `.vscode/tasks.json` のベストプラクティスを提示する。

`.vscode/tasks.json`

{
“version”: “2.0.0”,
“tasks”: [
{
“label”: “CMake: Configure (MinGW)”,
“type”: “shell”,
“command”: “cmake”,
“args”: [
“-B”, “build-mingw”,
“-G”, “Ninja”,
“-DCMAKE_TOOLCHAIN_FILE=/mingw64/share/cmake/Modules/CMakeToolchain.cmake”, // 適切にツールチェインを指定
“-DCMAKE_BUILD_TYPE=Release”
],
“group”: “none”
},
{
“label”: “CMake: Build & Package”,
“type”: “shell”,
“command”: “bash”,
“args”: [
“-c”,
“cmake –build build-mingw –config Release && ./package-dlls.sh build-mingw/myapp.exe ./dist”
],
“group”: {
“kind”: “build”,
“isDefault”: true
},
“problemMatcher”: [“$gcc”]
}
]
}

—

プロが教える隠しコマンド・キーボードショートカット

最後に、MSYS2やMinGWのCLI作業において、知っているだけで作業スピードが3倍跳ね上がる玄人向けの技を紹介する。

  • `Ctrl + R` (逆方向インクリメンタル検索)

Bashシェルで過去に叩いた複雑な `x86_64-w64-mingw32-g++` の長大なコマンドを呼び出す際、`history | grep` なんてやっている時間は無駄。`Ctrl + R` を押してコンパイルオプションのキーワードの断片(例: `-O3` や `-lssl`)を打ち込むだけで、瞬時に過去のコマンドが蘇る。

  • `MSYS2でのパス自動変換(Cygpath変換)の制御`

MinGWのシェルからWindowsネイティブのツールやパスを引数を渡して実行する際、MSYS2が勝手に `/c/Users/…` を `C:/Users/…` に書き換えてしまい困ることがある。
そんなときは、一時的に環境変数を設定してパス変換を無効化せよ:

MSYS_NO_PATHCONV=1 ./some_windows_tool.exe /path/to/unix/style

これを知っているだけで、WindowsとPOSIXのパスの差異で丸一日溶かすような無様なトラブルから解放される。

—

結びにかえて

開発基盤やビルド周りの自動化をケチるプロジェクトは、じわじわとチームの体力を奪っていく。「DLLがない」というプリミティブなエラーにエンジニアの大切な思考時間を奪わせてはならない。

今回紹介した依存DLL自動抽出シェルスクリプトを今日のビルドパイプラインに組み込み、チーム全体の生産性を圧倒的な高みへと引き上げてほしい。
君の健闘を祈る。

タイトルとURLをコピーしました