<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Feature-Flags on PHP Boy Scout</title><link>https://blog-570662.gitlab.io/tags/feature-flags/</link><description>Recent content in Feature-Flags on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Thu, 30 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog-570662.gitlab.io/tags/feature-flags/index.xml" rel="self" type="application/rss+xml"/><item><title>Two kinds of feature flag</title><link>https://blog-570662.gitlab.io/two-kinds-of-feature-flag/</link><pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/two-kinds-of-feature-flag/</guid><description>&lt;img src="https://blog-570662.gitlab.io/two-kinds-of-feature-flag/cover-two-kinds-of-feature-flag.png" alt="Featured image of post Two kinds of feature flag" /&gt;&lt;p&gt;go-tool-base has feature flags: switches that decide which built-in commands are live in a given run. rust-tool-base has those too. But it also has a second, completely separate kind of flag, and the difference between them is one of those distinctions that&amp;rsquo;s obvious the moment you see it and dangerously easy to conflate before you do. One decides what a command &lt;em&gt;does&lt;/em&gt;. The other decides whether a chunk of code is &lt;em&gt;in the binary at all&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="a-workspace-of-crates"&gt;A workspace of crates
&lt;/h2&gt;&lt;p&gt;Before the flags, the shape that makes them possible. go-tool-base is one Go module with packages under &lt;code&gt;pkg/&lt;/code&gt;. rust-tool-base is a Cargo &lt;em&gt;workspace&lt;/em&gt; of seventeen crates: &lt;code&gt;rtb-app&lt;/code&gt;, &lt;code&gt;rtb-config&lt;/code&gt;, &lt;code&gt;rtb-cli&lt;/code&gt;, &lt;code&gt;rtb-vcs&lt;/code&gt;, &lt;code&gt;rtb-ai&lt;/code&gt;, &lt;code&gt;rtb-mcp&lt;/code&gt;, &lt;code&gt;rtb-docs&lt;/code&gt;, &lt;code&gt;rtb-telemetry&lt;/code&gt;, and so on, with an umbrella crate called &lt;code&gt;rtb&lt;/code&gt; that re-exports the public surface.&lt;/p&gt;
&lt;p&gt;That isn&amp;rsquo;t tidiness for its own sake. Each subsystem being a separately compilable crate is what gives you a unit you can include or exclude &lt;em&gt;wholesale&lt;/em&gt;. Hold onto that, because it&amp;rsquo;s the hinge for everything below.&lt;/p&gt;
&lt;h2 id="the-flag-go-tool-base-already-has"&gt;The flag go-tool-base already has
&lt;/h2&gt;&lt;p&gt;go-tool-base has feature flags, and I&amp;rsquo;d describe them as runtime flags. A tool built on it can enable or disable built-in commands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetFeatures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Disable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InitCmd&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AiCmd&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At startup the framework resolves that set and decides which commands are reachable for this run. The &lt;code&gt;init&lt;/code&gt; command might be present in the binary but switched off; the &lt;code&gt;ai&lt;/code&gt; command might be switched on. It&amp;rsquo;s about the &lt;em&gt;user-facing surface&lt;/em&gt;: which commands exist for someone typing &lt;code&gt;--help&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;rust-tool-base keeps this idea. A command carries a &lt;code&gt;CommandSpec&lt;/code&gt; with an optional &lt;code&gt;feature&lt;/code&gt; field, and the runtime decides whether a feature-gated command is reachable. Same purpose: shape the surface per invocation.&lt;/p&gt;
&lt;p&gt;If that were the whole story, there&amp;rsquo;d be nothing to write. The reason there&amp;rsquo;s a post is the &lt;em&gt;other&lt;/em&gt; kind of flag, which Rust makes available and Go really doesn&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="the-flag-rust-adds"&gt;The flag Rust adds
&lt;/h2&gt;&lt;p&gt;Cargo features are a compile-time mechanism. The &lt;code&gt;rtb&lt;/code&gt; umbrella crate declares them like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cli&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;update&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;docs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;mcp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;credentials&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tui&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;cli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-cli&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-update&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-ai&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;rtb-docs?/ai&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;vcs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-vcs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;telemetry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-telemetry&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;full&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cli&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;update&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;docs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;mcp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ai&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;credentials&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tui&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;telemetry&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;vcs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each subsystem is an &lt;em&gt;optional&lt;/em&gt; crate dependency, and a feature switches it on. This is a different kind of switch entirely, and the difference is the whole point.&lt;/p&gt;
&lt;p&gt;A runtime flag decides what a command does &lt;em&gt;while the program runs&lt;/em&gt;. The code is in the binary either way; the flag just gates it.&lt;/p&gt;
&lt;p&gt;A Cargo feature decides what&amp;rsquo;s &lt;em&gt;in the binary in the first place&lt;/em&gt;. Build a tool without the &lt;code&gt;vcs&lt;/code&gt; feature and &lt;code&gt;rtb-vcs&lt;/code&gt; is not compiled. Its dependencies are not compiled. &lt;code&gt;gix&lt;/code&gt;, the pure-Rust Git implementation &lt;code&gt;rtb-vcs&lt;/code&gt; pulls in, roughly two and a half megabytes of it, is not compiled and not linked. It isn&amp;rsquo;t switched off in the binary. It was never in the binary. The compiler never even saw it.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s something a runtime flag cannot do, because by the time anything runs, the binary already exists with everything in it.&lt;/p&gt;
&lt;h2 id="two-axes-kept-separate"&gt;Two axes, kept separate
&lt;/h2&gt;&lt;p&gt;So rust-tool-base has two flag systems answering two genuinely different questions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cargo features&lt;/strong&gt; answer: &lt;em&gt;what is this binary made of?&lt;/em&gt; They&amp;rsquo;re decided when you build the tool, in &lt;code&gt;Cargo.toml&lt;/code&gt;. They control compilation, binary size, dependency surface, and compile time. A tool that never touches Git builds without &lt;code&gt;vcs&lt;/code&gt; and is smaller, faster to compile, and has a smaller dependency tree to audit. A tool that wants everything turns on &lt;code&gt;full&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Runtime feature flags&lt;/strong&gt; answer: &lt;em&gt;what can the user do in this run?&lt;/em&gt; They&amp;rsquo;re decided as the program starts. They control which commands appear, which paths are reachable.&lt;/p&gt;
&lt;p&gt;These could have been mashed into one mechanism, and it would have been a mistake. The app-context design notes are blunt about it: feature gating doesn&amp;rsquo;t belong on the per-command context object, because a feature-gated command &amp;ldquo;either exists or doesn&amp;rsquo;t&amp;rdquo; rather than changing its behaviour mid-run. Compile-time composition is one decision, made by the person building the tool. Runtime gating is another, made per invocation. Conflating them would mean you couldn&amp;rsquo;t reason cleanly about either.&lt;/p&gt;
&lt;h2 id="the-go-version-of-this-had-to-be-hand-built"&gt;The Go version of this had to be hand-built
&lt;/h2&gt;&lt;p&gt;This isn&amp;rsquo;t a thing Go simply lacks. I &lt;a class="link" href="https://blog-570662.gitlab.io/the-blank-import-that-keeps-a-dependency-out-of-your-binary/" &gt;wrote a whole post&lt;/a&gt; about how go-tool-base keeps its optional keychain dependency out of binaries that don&amp;rsquo;t want it, using a blank import and the linker&amp;rsquo;s dead-code elimination. It works. But it was a piece of deliberate engineering for &lt;em&gt;one&lt;/em&gt; dependency, and getting it right took care.&lt;/p&gt;
&lt;p&gt;Cargo features make that same outcome a first-class, declarative thing, and not for one dependency but for every subsystem the framework has. You don&amp;rsquo;t engineer the exclusion. You name a feature and leave it off. The crate, and its whole subtree, stays out. Rust&amp;rsquo;s build system was designed for exactly this, and rust-tool-base leans on it across the entire workspace rather than hand-rolling it once.&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us
&lt;/h2&gt;&lt;p&gt;go-tool-base has runtime feature flags: they decide, per invocation, which built-in commands are reachable. rust-tool-base keeps that, and adds a second kind that Rust makes available.&lt;/p&gt;
&lt;p&gt;Cargo features decide what the binary is &lt;em&gt;compiled from&lt;/em&gt;. Each of the framework&amp;rsquo;s seventeen crates is an optional dependency, and a feature switched off means that crate and its entire dependency subtree are never compiled or linked. A runtime flag gates what code &lt;em&gt;does&lt;/em&gt;; a Cargo feature gates whether code &lt;em&gt;is there at all&lt;/em&gt;. Two axes, two questions, deliberately kept as separate systems.&lt;/p&gt;</description></item></channel></rss>