はじめに
筆者は CMake があまり好きではありません。
- 最大の理由は
CMakeLists.txtの書き方が分からないことですが・・・ - Windows プラットホーム下でしか作業しない人にとっては大仰な仕様に思えます。しかし,MOZJPEG などのマルチプラットフォーム向けに開発されたライブラリを使用する場合には使用せざるを得ないのが悩ましいところです。
-
make cleanやcmake --build . --target cleanでは消せないファイル,具体的には CMake が生成するCMakeCache.txt,*.cmake,CMakeFiles/*などのファイルを削除する手段がデフォルトでは用意されていないのが不満です。
※同じ問題で悩まされている同志は多いようです12。
ということで,今回 CMake の作る大量の作業用ファイルを削除するバッチファイルを作ることにしました。なお,CMakeLists.txt など既存のファイルを修正することは有り得るので,既存のファイルは修正の有無に関わらずそのまま残します。
ホワイトリスト方式
CMake を実行する前に対象フォルダ以下のすべてのファイルおよびフォルダの一覧を作成しておきます。これをホワイトリストと呼ぶことにしましょう。CMake および make 実行後に対象フォルダ以下のすべてのファイル・フォルダを検索し,ホワイトリストに含まれているものは残し,含まれていないものは削除するというアルゴリズムを思いつきました。
もしも削除するほうのファイル・フォルダ一覧を記載したブラックリスト方式であればバッチファイルでも簡単に実現できます。一例を下記に示します。なお,厳密にはブラックリストに記載されたパス名がファイルかフォルダを区別して del コマンドと rd コマンドを使い分ける必要があります。
for %%I in (BLACKLIST.TXT) do if exist %%I del %%I
しかし,ホワイトリスト方式の場合,バッチファイルだと実現が難しいのです。PowerShell であればホワイトリストをハッシュテーブルにでも読み込んでチェックすれば良いのですが,バッチファイルだとほぼ不可能です。ファイルの数が少なければ下記の力業でも何とかなるとは思いますが・・・
setlocal enabledelayedexpansion
for /R . %%I in (*) do (
set FOUND=0
for %%J in (WHITELIST.TXT) do if /I "%%~I"=="%%~J" set FOUND=1
if !FOUND!==0 del "%%~I"
)
endlocal
その次に思いついたのはホワイトリスト記載のファイル・フォルダをいったん別の作業用フォルダに移動し,残ったファイル・フォルダをまとめて削除してしまうというものです。そして作業用フォルダの名前を変更(もしくはフォルダごと移動)すればよいのです。しかし,元のフォルダの相対的な位置関係を保持したまま別のフォルダに移動させるのが実に面倒なのです。※不可能ではないとは思います。
ちなみに XCOPY コマンドには除外ファイルリストを指定できる /EXCLUDE オプションというのがあります。しかしながら,コピー対象ファイルの絶対パスと除外ファイルリストに記載された文字列が部分一致しただけで除外されるという仕様のため,残念ながら使えません。
そして悩むこと4年・・・ついに先日妙案を思いつきました。Windows のファイルシステムにはファイル属性としてアーカイブ属性というのがあるので,これを利用する方法です。具体的には以下の手順を考えました。
- 指定されたファルダ以下のすべてのファイル・フォルダのアーカイブ属性をいったんセットします。これは
attribコマンド一発でできます。 - 次にホワイトリストに従ってファイル,あるいはフォルダのアーカイブ属性をクリアします。これは
forコマンドでループを回せば一行で済みます。ファイルの移動を伴わないのでさほど時間を要さないはずです。 - 続けて指定されたフォルダ以下にあるアーカイブ属性のセットされたファイルを削除します。これも
delコマンド一発でできます。残念ながらフォルダのほうは一発で削除できませんが,ファイルの数に比べたらさほど多くはないはずですから問題ないでしょう。 - 最後に削除されずに残ったファイルのアーカイブ属性をセットします。正しくは元の状態に戻すべきなのですが,それはそれで面倒なのでファイルのアーカイブ属性はセットし,フォルダはクリアとします。これも
attribコマンド一発でできます。
念のため AI に相談しました。
アーカイブ属性をユーザが使っていいのでしょうか?
⇒ 一時的に使用するのは問題ないでしょう。
お品書き(基本仕様案)
- スクリプトの引数は,①コマンド名,②対象フォルダ名,③ホワイトリストファイル名の3つです。
- ホワイトリストを作成するコマンド
CREATEとホワイトリストに従って不要ファイルを削除するコマンドREMOVEの二つを備えます。 - ホワイトリストには対象フォルダ以下の相対パスで記載するものとします。
- ホワイトリストに記載された全てのファイルが対象フォルダ以下に存在しない場合,エラーと判断して終了します。これは指定するフォルダを誤った場合,そのフォルダの中身をすべて削除してしまう可能性があるからです。
実装コード
気の短い人もいると思うので先に実装コードを示します。なお,SVG で貼りつけたので「新しいタブで画像を開く」とすれば部分的にコピー可能ですが,コード全体をコピーしたい人はこの後にコピー可能なコードを掲載していますのでそちらを参照下さい。
コピーできるコードはコチラ
@echo off
setlocal
rem ----------------------------------------------------------------------------
rem コマンドライン解析
rem ----------------------------------------------------------------------------
if "%~1"=="" goto USAGE
if "%~2"=="" goto USAGE
if "%~3"=="" goto USAGE
if exist "%~2" goto ENDIF
echo ディレクトリ %~2 が存在しません!!
exit /b 1
:ENDIF
set "ATTRIBUTE=%~a2"
if not "%ATTRIBUTE:D=%"=="%ATTRIBUTE%" goto ENDIF
echo %~2 はディレクトリではありません!!
exit /b 1
:ENDIF
set "DIRECTORY=%~f2"
if "%DIRECTORY:~-1%"=="\" set "DIRECTORY=%DIRECTORY:~0,-1%"
set "WHITELIST=%~f3"
if /I "%~1"=="CREATE" goto CREATE
if /I "%~1"=="REMOVE" goto REMOVE
echo コマンド %~1 には対応していません!!
exit /b 1
rem ----------------------------------------------------------------------------
rem ホワイトリストを作成
rem ----------------------------------------------------------------------------
:CREATE
pushd .
cd %DIRECTORY%
if exist "%WHITELIST%" del "%WHITELIST%"
for /R . %%I in (*) do call :OUTPUT_WHITELIST "%%~I"
for /D /R . %%I in (*) do call :OUTPUT_WHITELIST "%%~I"
popd
exit /b 0
rem ----------------------------------------------------------------------------
rem ホワイトリストを出力
rem ----------------------------------------------------------------------------
:OUTPUT_WHITELIST
setlocal enabledelayedexpansion
set "FULLPATH=%~1"
set "FULLPATH=!FULLPATH:%DIRECTORY%=.!"
>>"%WHITELIST%" echo !FULLPATH!
endlocal
exit /b 0
rem ----------------------------------------------------------------------------
rem ホワイトリストに存在しないファイルを削除
rem ----------------------------------------------------------------------------
:REMOVE
if exist "%~3" goto ENDIF
echo ホワイトリスト %~3 が存在しません!!
exit /b 1
:ENDIF
pushd .
cd %DIRECTORY%
rem ----------------------------------------------------------------------------
rem 対象フォルタとホワイトリストの比較
rem ----------------------------------------------------------------------------
set NOT_FOUND=0
for /F "usebackq delims=" %%I in ("%WHITELIST%") do if not exist "%%~I" set /a NOT_FOUND+=1
if %NOT_FOUND%==0 goto ENDIF
echo 対象フォルダとホワイトリストの内容が一致していません!!
popd
exit /b 1
:ENDIF
rem ----------------------------------------------------------------------------
rem 対象フォルダ以下のすべてのファイル・フォルダのアーカイブ属性をセットする
rem ----------------------------------------------------------------------------
attrib +A /S /D *
rem ----------------------------------------------------------------------------
rem ホワイトリストに記されたファイル・フォルダのアーカイブ属性をクリアする
rem ----------------------------------------------------------------------------
for /F "usebackq delims=" %%I in ("%WHITELIST%") do attrib -A /D "%%~I"
rem ----------------------------------------------------------------------------
rem 対象フォルダ以下のアーカイブ属性のセットされた全てのファイルを削除する
rem ----------------------------------------------------------------------------
2>NUL del /S /Q /AA *
rem ----------------------------------------------------------------------------
rem 対象フォルダ以下のアーカイブ属性のセットされた全てのフォルダを削除する
rem ----------------------------------------------------------------------------
for /D /R . %%I in (*) do call :REMOVE_FOLDER "%%~I"
rem ----------------------------------------------------------------------------
rem 対象フォルダ以下のすべてのファイルのアーカイブ属性をセットする
rem ----------------------------------------------------------------------------
attrib +A /S *
popd
exit /b 0
rem ----------------------------------------------------------------------------
rem アーカイブ属性のセットされたフォルダを削除する
rem ----------------------------------------------------------------------------
:REMOVE_FOLDER
set "ATTRIBUTE=%~a1"
if "%ATTRIBUTE:A=%"=="%ATTRIBUTE%" exit /b 0
echo 削除したフォルダ - %~1
rd /s /q "%~1"
exit /b 0
rem ----------------------------------------------------------------------------
rem ヘルプメッセージ
rem ----------------------------------------------------------------------------
:USAGE
echo ホワイトリストを作成し,リストに存在しないファイルを削除します。
echo.
echo %~n0(%~x0) [コマンド] [対象フォルダ] [ホワイトリスト]
echo.
echo コマンド:
echo CREATE:対象フォルダ以下の全てのファイル・フォルダを検索し,
echo ホワイトリストを作成します。
echo REMOVE:対象フォルダ以下の全てのファイル・フォルダを検索し,
echo ホワイトリストに存在しないファイルを削除します。
exit /b -1
使い方
引数なしで実行するとヘルプメッセージを表示します。
c:\Qiita>wlutil
ホワイトリストを作成し,リストに存在しないファイルを削除します。
WLUTIL(.CMD) [コマンド] [対象フォルダ] [ホワイトリスト]
コマンド:
CREATE:対象フォルダ以下の全てのファイル・フォルダを検索し,
ホワイトリストを作成します。
REMOVE:対象フォルダ以下の全てのファイル・フォルダを検索し,
ホワイトリストに存在しないファイルを削除します。
使用するときは事前にホワイトリストを作成します。
c:\Qiita>wlutil create mozjpeg mozjpeg-list.txt
CMake 実行後,ホワイトリストに基づいてフォルダのクリーンアップを行った場合です。
c:\Qiita>wlutil remove mozjpeg mozjpeg-list.txt
削除したファイル - c:\Qiita\mozjpeg\CMakeCache.txt
削除したファイル - c:\Qiita\mozjpeg\cmake_install.cmake
削除したファイル - c:\Qiita\mozjpeg\cmake_uninstall.cmake
削除したファイル - c:\Qiita\mozjpeg\croptest
削除したファイル - c:\Qiita\mozjpeg\CTestTestfile.cmake
削除したファイル - c:\Qiita\mozjpeg\installer.nsi
削除したファイル - c:\Qiita\mozjpeg\jconfig.h
削除したファイル - c:\Qiita\mozjpeg\jconfigint.h
削除したファイル - c:\Qiita\mozjpeg\jversion.h
削除したファイル - c:\Qiita\mozjpeg\Makefile
~中略~
削除したファイル - c:\Qiita\mozjpeg\simd\CMakeFiles\simd.dir\progress.make
削除したファイル - c:\Qiita\mozjpeg\win\jpeg.rc
削除したファイル - c:\Qiita\mozjpeg\win\mozjpegTargets-release.cmake
削除したファイル - c:\Qiita\mozjpeg\win\mozjpegTargets.cmake
削除したファイル - c:\Qiita\mozjpeg\win\turbojpeg.rc
削除したフォルダ - c:\Qiita\mozjpeg\CMakeFiles
削除したフォルダ - c:\Qiita\mozjpeg\pkgscripts
削除したフォルダ - c:\Qiita\mozjpeg\md5\CMakeFiles
削除したフォルダ - c:\Qiita\mozjpeg\sharedlib\CMakeFiles
削除したフォルダ - c:\Qiita\mozjpeg\simd\CMakeFiles
最後に
このスクリプトの名称について AI と相談しました。
- CMakeCleaner:CMake の作ったゴミを掃除するために作りましたが,用途は別に CMakeに限らないはずです。
- CosmoCleaner:汚染された国土を元の状態へ復帰する装置ではありますが,さすがに大仰過ぎますね。
- WhiteListCleaner:残すべきファイル一覧を筆者はホワイトリストと呼んでおり,それに基づいて削除するので。ただし AI は除外リスト Exclusive List と呼びます。
以上より,作者本人が馴染んでいませんが White List UTILity としました。略して WLUTIL です。これは削除するだけなく,ホワイトリストを作成する機能もあるからです。