symlink と Property Sheet で Visual Studio の C++ プロジェクトの構成を整理するメモ
Windows向けアプリの比較的古いプロジェクトを扱う事になると「構成」がカオスで整理したくなる事がしばしばあります。または、これから新しく作るプロジェクトについて、構成が複雑になりそうな場合の参考にもなるかもしれないので、その整理方法についてメモを残しておきます。
このメモでは、
- 外部依存ライブラリーのインクルードパスをどのように整理するか? --> symlink がおすすめです
- プリプロセッサーの定義をどのように整理するか? --> Visual Studio の Property Sheet の User Macro がおすすめです
について簡単に解説します。
前提
- Windows 10 でもいまでは
symlink
を使えます( junction とは別に symlink が使えるようになっています)- NTFS 上で自然に扱え、 Git for Windows / Git Bash / cmd / PowerShell などで確認や操作できます
- 少しだけ注意点があります
- Visual Studio の C++ 開発プロジェクトでも Property Sheet の
User Macros
を使えます
このメモの対象は、管理する構成、外部依存ライブラリー、ビルド時のプリプロセッサー定義が多いプロジェクト向けです。逆に、例えば、x64向けのDEBUG/RELEASEの2パターンしかなく、外部依存ライブラリーもせいぜい1つか2つ、ソースコードレベルのビルドパターンを分けるための #ifdef
用のフラグも存在しないか、せいぜい1つか2つしかない、そのようなプロジェクトではかえって煩雑になるだけの可能性もあるので、必要を見極めた上で、方法の採否を決めてください。
整理メモ
1. 外部依存ライブラリーのインクルードパスをどのように整理するか?
プロジェクトの構成がカオスに陥っている場合、よくある現象の1つに Additional Include Directories (追加のインクルードディレクトリー)が次のように混乱している事がしばしばあります:
..\..\common\libAAA;C:\Python33\include;library\my_old_mystic_lib;libarry\my_new_lib\include;library2/boost_1_71_0;library2\lpng1637;%(AdditionalIncludeDirectories)
セパレーター文字;
で分解すると、
..\..\common\libAAA
← だぶん複数プロジェクトで使いまわしているライブラリー置き場にあるライブラリーC:\Python33\include
← Windowsでは"おれおれポジション"な場所へ標準でインストールされるはずのライブラリーlibrary\my_old_mystic_lib
← プロジェクト内で初期に整理された秘伝のおれおれライブラリーlibarry\my_new_lib\include
← プロジェクト中期におれおれ的には新設計で追加されたと思われる新型のおれおれライブラリーlibrary2/boost_1_71_0
← プロジェクトを最近になってテコ入れしようと思って追加したと思われるライブラリーlibrary2\lpng1637
← プロジェクトをテコ入れしたらユーザーニーズが発生して新たに必要になって追加したと思われるライブラリー%(AdditionalIncludeDirectories)
← Visual Studio がいつの頃からかつけるようになったよくわからないから触れずにつけてあるおまじない(*1)
と、いった具合です。さらに、プロジェクトの構成がデバッグ、リリースだけではなく、デバッグ・アレ特殊版、リリース・ソレ特殊版、実験用1、実験用2…など増えていて、それにさらにx86、x64があり…。MSVC++のプロジェクトではしばしば、ありがちです。
このメモでは symlink で↑を整理し保守性を改善する方法の提案についてメモを残します。
(*1): %(AdditionalIncludeDirectories)
の正体はもちろん"オマジナイ"ではなく意味のある値で、次の (2) で間接的には触れますが、それにフォーカスした詳細の解説はこのメモの趣旨ではないので行いません。
symlink
による Additional Include Directories の整理
整理方法:
- プロジェクトのルートに
include
ディレクトリーを作ります - プロジェクトのルートに
external_libraries
ディレクトリーを作ります - プロジェクトに固有のライブラリー群を
external_libraries
へ放り込みます include
からmklink
(cmdなど) またはln
(Git Bashなど) 等を使い、external_libraries
(または..\common\
など)へ相対パスのsymlink
を作成します- ライブラリーの管理方法はこのようにする事が開発に関わる人がわかりやすいように README.md などに記しておきます
こうすると、理想的には、 Visual Studio の C++ プロジェクトの Additional Include Directory は原則的に全ての構成で共通して
.\include;%(AdditionalIncludeDirectories)
だけで済みます(*5)。ライブラリーのバージョンアップのたびにすべての構成の Additional Include Directory の設定値を変更する必要が無くなり、作業コストも減り、構成ごとの設定値を同期し忘れる事故も安全に防げます。
注意点として、この方法に統一したい場合に、 libpng のようにディレクトリーのワンクッションが無くソースコードからの #include <png.h>
のように使っていた、あるいは使いたいと考えていた場合、ソースコードを #include <png/png.h>
のように変更する必要が生じます。これを避けたい場合、ファイル1つだけで済む程度ならばファイル単位の symlink
としても事実上の作業コストは変わりませんが、もし複数のファイルをそのように扱う必要がある場合は諦めてワンクッション入れる事をおすすめします。(*4)
なお、外部依存ライブラリーそのものの管理方法も Git Submodule で体系化したりできますが、それは構成の話とは別になるのでこのメモでは触れません。
(*1): ぱっと見の精神衛生を気にすれば boost-1.71.0
と libpng-1.6.37
あるいは他の統一された命名規則を用意して揃えたくなるかもしれませんが、このメモではおすすめしません。もしそのようなルールを採用する場合は、保守コストが増大したり、複数人の開発者が関わる場合に誤った命名に注意を払う必要が生じたり、そのような面倒の方が大きい事を覚悟した上で導入します。また、もし、同じライブラリーでも複数のバージョンを併存させてビルド構成ごとに分けたい場合などは、 external_libraries/boost/boost_1_71_0
のようにワンクッション入れて整理したくなるかもしれませんが、多くの場合、1つのライブラリーにそれほどたくさんのバージョンを併存させる必要はありませんし、バージョンが併存して扱われている可能性も視認し難くなり、保守コストも増加するので同様にこのメモではおすすめしません。
(*2): 例:
include/GLFW
->../external_libraries/glfw-3.3.bin.WIN64/include
GLFWinclude/xl
->../external_libraries/libxl-3.8.8.2/include_cpp
LibXL
(*3): 例:
include/boost
->../external_libraries/boost_1_71_0
Boostinclude/png
->../external_libraries/lpng1637
libpng
(*4): 例えば ligpng のように配布物を展開するとソースとヘッダーが整理されずに配置されている場合などありますが、バージョンの切り替えの際の手間が増え面倒となり、さらにスクリプトを作り半自動化したくなるかもしれません。コスト増加に合理的な理由がない場合はディレクトリー単位で簡単な symlink
を用意するだけに留める事をこのメモではおすすめします。どうしても、特定のライブラリーについて直接展開先のディレクトリー直下のファイルを #include <something.h>
のように扱いたい場合は、次の (2) でメモを残す Property Sheet に実際のパスを定義する Key-Value を定義した上で、 Additional Include Directories ではその変数を参照させる事で、ライブラリーのバージョンアップのたびに複数の構成の設定値を書き換える手間がかからないようにはできます。
おまけ: Windows や Git Bash で symlink を使える状態に設定にする方法
- Windows10を開発者モードにする(設定→更新とセキュリティー→開発者向け(画面左側)→開発者モード)
- cmd で
setx MSYS winsymlinks:nativestrict
- Git Bash で
git config --global core.symlinks true
2. プリプロセッサーの定義をどのように整理するか?
構成がカオス化したプロジェクトでは Preprocessor の定義もカオス化していることがしばしばあります。複数のリリースパターンが存在するプロジェクトで #ifdef
でソースコードレベルでビルドを分岐している場合によく起こります。例えば、
FLAG_X
, FLAG_Y
, FLAG_P
, FLAG_Q
, FLAG_R
, ... などたくさんのフラグが…
- X + P ← 初期からある組み合わせ
- Y + Q ← 初期からある組み合わせ
- X + P + Q ← ある時点で追加した組み合わせ
- Y + P + Q + R ← 最近追加した組み合わせ
のようにあり、これらを /D
オプション( = Preprocessor の設定値 )で渡してフラグを切り替えた別バージョンとしてビルド、リリースしていた場合、
FLAG_S
を追加する事にし、これは X + P + Q + S の組み合わせでリリースする事にした…FLAG_Q
を廃止する事にした…
そんなような事が起きている事があります。あるいは、デバッグビルドでは特別なフラグを設定して運用している、とか。
何れにせよフラグの管理が合理化されておらず、すべての構成ですべてのフラグを直接管理していると、特にフラグが多ければ多いほど構成の追加や変更に伴いフラグ管理のミスによる誤ったビルド、リリースによる事故が起きたり、そうでなくともたいていセットで使うフラグをプロジェクトの新人がうっかり知らずに片方だけONにしてトラブルになったり…そのような問題に繋がりやすくなります。
Visual Studio の構成の Preprocessor 項のように文字列を与えられる部分ではUser Macros
や環境変数を使い設定値を整理できます。 (1)で symlink
を使ったので、"その手"の手段としては環境変数も使える手段ですが、 Windows で Visual Studio の msbuild
を使う場合にはあまり便利ではありません。このメモでは User Macros
を使い整理する手法を残します。
User Macros
( Property Manager, Property Sheet, .props ) の仕組みと使い方
View
(表示)からProperty Manager
を表示しますProperty Manager
にAdd New Project Property Sheet
ボタンがあるので、ぽちってMyFlag.props
など適当な名前のプロパティーシートを追加します(この名前は簡単に後でも変えられます)MyFlag
のプロパティーを Visual Studio で開きます(*1)User Macros
を開き、プロジェクトの構成で整理したい文字列の設定値を作ります(*2)- プロジェクトのプロパティーを開き、(4)で整理(定義)した文字列値を使いたい設定値の部分で
$(MY_DEBUG)
の形式で使います(*3) - 整理されたフラグはプロパティーマネージャーを使う事を開発に関わる人がわかりやすいように README.md などに記しておきます
こうすると、たくさんの /D
オプション用のフラグ管理と構成が必要な場合に、セットで使うはずのフラグの設定漏れをある程度防いだり、構成を追加する最に構成の設定値上で記述するフラグの数を少なくして手間を省いたりできます。
この管理方法を使うと、フラグの削除が必要になった場合の手間はもしかしたら僅かですが増えるかもしれません。ファイル1つからフラグを置換(削除)するか、ファイル2つからするか、というだけの違いなので、何れにしても Visual Studio や VSCode の強力で便利でプレビュー付きで安全な正規表現でまとめて処理するのでさほど…とは思いますが、一応書いておきます。
(*1): プロジェクトのプロパティーの画面とほぼ同じなので混乱しそうですが、プロパティーマネージャーの構成とプロジェクトの構成は別に管理されているものなので慌てないでください
(*2): 単純なKey-Valueの文字列変数です。例えば、 MY_DEBUG=_DEBUG;MY_EXTRA_DEBUG_FLAG
とか、 FLAG_PQ=FLAG_P;FLAG_Q
とか。
(*3): msbuild 向けの "変数的なそれ" の展開方法は3種類あります:
- Property element へのアクセス =
$(Key)
<-- 今回 (2) で Property Sheet のUser Macros
に追加した Key-Value で使いました - Item element へのアクセス =
@(Key)
- Item Metadata へのアクセス =
%(Key)
<-- Visual Studio の作るプロジェクトの構成のAdditional Include Directories
に標準で追加されている%(AdditionalIncludeDirectories)
はこのパターンです (*4)
(*4): プロジェクトの構成にみられる `%(AdditionalIncludeDirectories)
はプロジェクトの構成( = .vcxproj)内で循環参照しているわけではなく、別ファイル管理のプロパティーシート( = .props)群の Additional Include Directories
群をバッチ的に展開しています。
参考
- symlink
- いまどきの Windows 10 開発者モードの参考
- もう少しだけ面倒だった頃の参考
- Property Sheet