feature flagがbuild tool configを制御しない時はoverlayを使う
feature flagは、多くの場合application codeを制御します。しかし、buildに参加するすべてのtoolを自動で制御するわけではありません。
compiler、bundler、packager、linker、deploy toolがそれぞれ独自の設定ファイルを読む場合、この違いが問題になります。code上ではfeatureを無効にしていても、build toolがそのfeature用の設定を読んで動くことがあります。
その時は、configurationをbase fileとfeature-specific overlayに分けます。
症状: featureはoffなのにbuild toolが動く
失敗は矛盾して見えます。
- optional featureはdisabled
- code pathはcompile-time gateの内側
- それでもbuildがsecret、signing key、endpoint、asset、plugin settingを要求する
- errorはcompile後、packagingやdeploymentで出る
よくある形はこうです。
feature OFF
compiled code path removed
build tool config still present
build tool performs feature-specific work
これはfeature flagのbugではありません。境界のbugです。
compile-time gateとtool-level configを分けて考える
compile-time gateはsource codeを制御します。
例:
- Rust
#[cfg(feature = "updater")] - C/C++ preprocessor flag
- bundlerで置換されるTypeScript constant
- package-level optional import
tool-level configはbuild toolを制御します。
例:
- bundle target
- signing setting
- updater artifact setting
- deploy destination
- plugin configuration
- linker script
- generated asset option
build toolは、あなたのcode feature flagが何を意味するかを必ず知っているわけではありません。config fileに「このartifactに署名する」「このpluginを有効にする」と書いてあれば、code pathがcompileされたかどうかに関係なく、その作業を試みることがあります。
base configとfeature overlayを使う
base configには、すべてのvariantで共通する設定だけを残します。
{
"name": "my-app",
"build": {
"outDir": "dist"
},
"bundle": {
"targets": ["installer"]
}
}
optional settingは別のoverlayに置きます。
{
"bundle": {
"createUpdateArtifacts": true
},
"release": {
"signing": {
"required": true
}
}
}
build commandも明示します。
# Base build
build-tool --config config.json
# Feature build
build-tool --config config.json --config config.updater.json
複数configを直接mergeできるtoolもあれば、先にcombined configを生成する必要があるtoolもあります。原則は同じです。base settingは常に読み、feature settingはそのvariantでだけ読みます。
overlayに頼る前にmerge semanticsを確認する
すべてのtoolがJSONを同じようにmergeするとは限りません。
RFC 7386で定義されるJSON Merge Patchでは、objectはmergeされ、nullは削除を意味し、arrayはappendではなくreplaceされる値として扱われます。
例えばbaseがこれで:
{
"bundle": {
"targets": ["msi", "nsis"],
"active": true
}
}
overlayがこれなら:
{
"bundle": {
"targets": ["nsis"]
}
}
結果はこうなり得ます。
{
"bundle": {
"targets": ["nsis"],
"active": true
}
}
arrayは追加ではなく置換されています。
release processでoverlayを使う前に、次を確認します。
- objectは再帰的にmergeされるか
- arrayはreplaceかappendか
nullはkey削除か- 後のconfigが前のconfigを上書きするか
- unknown keyをvalidateするか
overlay fileは小さくし、変更するkeyだけを入れます。
build後にvariant-specific artifactを確認する
build successだけでは足りません。overlayを忘れても、間違ったvariantのvalid artifactができることがあります。
artifact checkを入れます。
# Base build should produce the app but not the optional artifact.
test -f dist/my-app.exe
test ! -f dist/my-app.exe.sig
# Updater build should produce both.
test -f dist/my-app.exe
test -f dist/my-app.exe.sig
pathは使っているtoolに合わせます。verificationは「commandがexit 0だった」ではなく、variantのcontractを表すべきです。
よいcheckは具体的です。
- default buildにはupdater signatureがない
- updater buildにはupdater signatureがある
- free buildにはpaid-only assetがない
- production buildはproduction endpointを使う
- staging buildはproduction credentialを使わない
目的は、間違ったvariantで成功してしまうことを捕まえることです。
secretをbase configに置かない
config overlayは安全境界でもあります。
base configにoptional release secretを含めないでください。すべてのbuildで必要なpublic settingなら置けますが、local developerにproduction signing keyやdeploy credentialを要求する形にしないほうがよいです。
こちらを選びます。
config.json
config.release.json
こちらは避けます。
config.json with release signing enabled by default
後者はrelease pathを誤ってtriggerしやすくし、development pathを無駄に壊しやすくします。
overlayが過剰なケース
小さなoptionすべてを分割する必要はありません。
overlayが有効なのは、次のような場合です。
- buildに名前付きvariantがある
- variantごとにartifactが違う
- variantごとに必要なsecretが違う
- featureがlocal developmentではoptional
- wrong variantでもcommandが成功し得る
その設定がすべてのenvironmentで常に必要なら、base configに置きます。overlayが多すぎると、build全体を理解しにくくなります。
参考
- RFC 7386: JSON Merge Patch
- Tauri configuration reference
- Tauri v2 Updaterビルドは設定overlayを分ける
- Viteのenv変数が更新されない時はdev server再起動とcacheを確認する
まとめ
feature flagはcodeを制御します。build toolはartifactを制御します。この2つが同じgateを共有していないなら、base configとfeature overlayに分け、merge ruleを確認し、variantを定義するartifactを検証します。