diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt
index 8252b10b7..56142fc56 100644
--- a/.github/actions/spelling/allow/apis.txt
+++ b/.github/actions/spelling/allow/apis.txt
@@ -45,6 +45,7 @@ IBind
IBox
IClass
IComparable
+IComparer
IConnection
ICustom
IDialog
@@ -85,6 +86,7 @@ NOREPEAT
ntprivapi
oaidl
ocidl
+ODR
osver
OSVERSIONINFOEXW
otms
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 722631b26..fc07e9d2a 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -163,6 +163,7 @@ BPBF
bpp
BPPF
branchconfig
+brandings
BRK
Browsable
bsearch
diff --git a/build/rules/GenerateFeatureFlags.proj b/build/rules/GenerateFeatureFlags.proj
new file mode 100644
index 000000000..8a8b494a6
--- /dev/null
+++ b/build/rules/GenerateFeatureFlags.proj
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+ Release
+ AnyCPU
+
+
+ Fuzzing
+ AnyCPU
+
+
+ AuditMode
+ AnyCPU
+
+
+ Debug
+ AnyCPU
+
+
+
+
+ d97c3c61-53cd-4e72-919b-9a0940e038f9
+
+
+
+ $(SolutionDir)obj\$(Configuration)\GenerateFeatureFlags\
+ $(SolutionDir)bin\$(Configuration)\
+
+ <_WTBrandingName Condition="'$(WindowsTerminalBranding)'=='Preview'">Preview
+ <_WTBrandingName Condition="'$(WindowsTerminalBranding)'=='Release'">Release
+ <_WTBrandingName Condition="'$(_WTBrandingName)'==''">Dev
+
+
+
+
+
+
+
+
+ <_BrandingLines Include="$(_WTBrandingName)" />
+
+
+
+
+
+
+ <_BranchBrandingCacheFiles Include="$(IntermediateOutputPath)branch_branding_cache.txt" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/feature_flags.md b/doc/feature_flags.md
new file mode 100644
index 000000000..f4c2a7d68
--- /dev/null
+++ b/doc/feature_flags.md
@@ -0,0 +1,65 @@
+# til::feature
+
+Feature flags are controlled by an XML document stored at `src/features.xml`.
+
+## Example Document
+
+```xml
+
+
+
+
+ Feature_XYZ
+
+ Does a cool thing
+
+
+ 1234
+
+
+ AlwaysEnabled|AlwaysDisabled
+
+
+
+ branch/with/wildcard/*
+
+
+
+
+
+ ...
+
+
+
+
+
+ Release
+
+
+
+
+
+ ...
+
+
+
+
+
+
+```
+
+## Notes
+
+Features that are disabled for Release using `alwaysDisabledReleaseTokens` are
+*always* disabled in Release, even if they come from a branch that would have
+been enabled by the wildcard.
+
+### Precedence
+
+1. `alwaysDisabledReleaseTokens`
+2. Enabled branches
+3. Disabled branches
+ * The longest branch token that matches your branch will win.
+3. Enabled brandings
+4. Disabled brandings
+5. The feature's default state
diff --git a/src/common.build.post.props b/src/common.build.post.props
index 87416a898..e5bc2f5f3 100644
--- a/src/common.build.post.props
+++ b/src/common.build.post.props
@@ -64,4 +64,21 @@
+
+
+
+ OCCallFeatureFlagGenerator;
+ $(BuildDependsOn)
+
+
+
+
+
+
+
+
+
+ $(SolutionDir)\bin\$(Configuration)\inc\TilFeatureStaging.h;%(ForcedIncludeFiles)
+
+
diff --git a/src/features.xml b/src/features.xml
new file mode 100644
index 000000000..7f11a0ba2
--- /dev/null
+++ b/src/features.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ Feature_ExampleFeat
+ This feature will be replaced in the next pull request.
+ AlwaysEnabled
+
+ Preview
+ WindowsInbox
+
+
+
diff --git a/tools/FeatureStagingSchema.xsd b/tools/FeatureStagingSchema.xsd
new file mode 100644
index 000000000..3e6819244
--- /dev/null
+++ b/tools/FeatureStagingSchema.xsd
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/Generate-FeatureStagingHeader.ps1 b/tools/Generate-FeatureStagingHeader.ps1
new file mode 100644
index 000000000..8206b7bb1
--- /dev/null
+++ b/tools/Generate-FeatureStagingHeader.ps1
@@ -0,0 +1,206 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT license.
+
+################################################################################
+# This script generates a header describing which Terminal/Console features
+# should be compiled-in, based on an XML document describing them.
+
+[CmdletBinding()]
+Param(
+ [Parameter(Position=0, Mandatory=$True)]
+ [ValidateScript({ Test-Path $_ })]
+ [string]$Path,
+
+ [ValidateSet("Dev", "Preview", "Release", "WindowsInbox")]
+ [string]$Branding = "Dev",
+
+ [string]$BranchOverride = $Null,
+
+ [string]$OutputPath
+)
+
+Enum Stage {
+ AlwaysDisabled;
+ AlwaysEnabled;
+}
+
+Function ConvertTo-FeatureStage([string]$stage) {
+ Switch($stage) {
+ "AlwaysEnabled" { [Stage]::AlwaysEnabled; Return }
+ "AlwaysDisabled" { [Stage]::AlwaysDisabled; Return }
+ }
+ Throw "Invalid feature stage $stage"
+}
+
+Class Feature {
+ [string]$Name
+ [Stage]$Stage
+ [System.Collections.Generic.Dictionary[string, Stage]]$BranchTokenStages
+ [System.Collections.Generic.Dictionary[string, Stage]]$BrandingTokenStages
+ [bool]$DisabledReleaseToken
+
+ Feature([System.Xml.XmlElement]$entry) {
+ $this.Name = $entry.name
+ $this.Stage = ConvertTo-FeatureStage $entry.stage
+ $this.BranchTokenStages = [System.Collections.Generic.Dictionary[string, Stage]]::new()
+ $this.BrandingTokenStages = [System.Collections.Generic.Dictionary[string, Stage]]::new()
+ $this.DisabledReleaseToken = $Null -Ne $entry.alwaysDisabledReleaseTokens
+
+ ForEach ($b in $entry.alwaysDisabledBranchTokens.branchToken) {
+ $this.BranchTokenStages[$b] = [Stage]::AlwaysDisabled
+ }
+
+ # AlwaysEnabled branches win over AlwaysDisabled branches
+ ForEach ($b in $entry.alwaysEnabledBranchTokens.branchToken) {
+ $this.BranchTokenStages[$b] = [Stage]::AlwaysEnabled
+ }
+
+ ForEach ($b in $entry.alwaysDisabledBrandingTokens.brandingToken) {
+ $this.BrandingTokenStages[$b] = [Stage]::AlwaysDisabled
+ }
+
+ # AlwaysEnabled brandings win over AlwaysDisabled brandings
+ ForEach ($b in $entry.alwaysEnabledBrandingTokens.brandingToken) {
+ $this.BrandingTokenStages[$b] = [Stage]::AlwaysEnabled
+ }
+ }
+
+ [string] PreprocessorName() {
+ return "TIL_$($this.Name.ToUpper())_ENABLED"
+ }
+}
+
+class FeatureComparer : System.Collections.Generic.IComparer[Feature] {
+ [int] Compare([Feature]$a, [Feature]$b) {
+ If ($a.Name -lt $b.Name) {
+ Return -1
+ } ElseIf ($a.Name -gt $b.Name) {
+ Return 1
+ } Else {
+ Return 0
+ }
+ }
+}
+
+Function Resolve-FinalFeatureStage {
+ Param(
+ [Feature]$Feature,
+ [string]$Branch,
+ [string]$Branding
+ )
+
+ # RELEASE=DISABLED wins all checks
+ # Then, branch match by most-specific branch
+ # Then, branding type (if no overriding branch match)
+
+ If ($Branding -Eq "Release" -And $Feature.DisabledReleaseToken) {
+ [Stage]::AlwaysDisabled
+ Return
+ }
+
+ If (-Not [String]::IsNullOrEmpty($Branch)) {
+ $lastMatchLen = 0
+ $branchStage = $Null
+ ForEach ($branchToken in $Feature.BranchTokenStages.Keys) {
+ # Match the longest branch token -- it should be the most specific
+ If ($Branch -Like $branchToken -And $branchToken.Length -Gt $lastMatchLen) {
+ $lastMatchLen = $branchToken.Length
+ $branchStage = $Feature.BranchTokenStages[$branchToken]
+ }
+ }
+ If ($Null -Ne $branchStage) {
+ $branchStage
+ Return
+ }
+ }
+
+ $BrandingStage = $Feature.BrandingTokenStages[$Branding]
+ If ($Null -Ne $BrandingStage) {
+ $BrandingStage
+ Return
+ }
+
+ $Feature.Stage
+}
+
+$ErrorActionPreference = "Stop"
+$x = [xml](Get-Content $Path -EA:Stop)
+$x.Schemas.Add('http://microsoft.com/TilFeatureStaging-Schema.xsd', (Resolve-Path (Join-Path $PSScriptRoot "FeatureStagingSchema.xsd")).Path) | Out-Null
+$x.Validate($null)
+
+$featureComparer = [FeatureComparer]::new()
+$features = [System.Collections.Generic.List[Feature]]::new(16)
+
+ForEach ($entry in $x.featureStaging.feature) {
+ $features.Add([Feature]::new($entry))
+}
+
+$features.Sort($featureComparer)
+
+$featureFinalStages = [System.Collections.Generic.Dictionary[string, Stage]]::new(16)
+
+$branch = $BranchOverride
+If ([String]::IsNullOrEmpty($branch)) {
+ Try {
+ $branch = & git branch --show-current 2>$Null
+ } Catch {
+ Try {
+ $branch = & git rev-parse --abbrev-ref HEAD 2>$Null
+ } Catch {
+ Write-Verbose "Cannot determine current Git branch; skipping branch validation"
+ }
+ }
+}
+
+ForEach ($feature in $features) {
+ $featureFinalStages[$feature.Name] = Resolve-FinalFeatureStage -Feature $feature -Branch $branch -Branding $Branding
+}
+
+### CODE GENERATION
+
+$script:Output = ""
+Function AddOutput($s) {
+ $script:Output += $s
+}
+
+AddOutput @"
+// THIS FILE IS AUTOMATICALLY GENERATED; DO NOT EDIT IT
+// INPUT FILE: $Path
+
+"@
+
+ForEach ($feature in $features) {
+ $stage = $featureFinalStages[$feature.Name]
+
+ AddOutput @"
+#define $($feature.PreprocessorName()) $(If ($stage -eq [Stage]::AlwaysEnabled) { "1" } Else { "0" })
+
+"@
+}
+
+AddOutput @"
+
+#if defined(__cplusplus)
+
+"@
+
+ForEach ($feature in $features) {
+ AddOutput @"
+__pragma(detect_mismatch("ODR_violation_$($feature.PreprocessorName())_mismatch", "$($feature.Stage)"))
+struct $($feature.Name)
+{
+ static constexpr bool IsEnabled() { return $($feature.PreprocessorName()) == 1; }
+};
+
+"@
+}
+
+AddOutput @"
+#endif
+"@
+
+If ([String]::IsNullOrEmpty($OutputPath)) {
+ $script:Output
+} Else {
+ Out-File -Encoding UTF8 -FilePath $OutputPath -InputObject $script:Output
+}