feature/featuretags,cmd/omitsize: support feature dependencies

This produces the following omitsizes output:

    Starting with everything and removing a feature...

    tailscaled tailscale combined (linux/amd64)
     27005112  18153656  39727288
    - 7696384 - 7282688 -19607552 .. remove *
    -  167936 -  110592 -  245760 .. remove acme
    - 1925120 -       0 - 7340032 .. remove aws
    -    4096 -       0 -    8192 .. remove bird
    -   20480 -   12288 -   32768 .. remove capture
    -       0 -   57344 -   61440 .. remove completion
    -  249856 -  696320 -  692224 .. remove debugeventbus
    -   12288 -    4096 -   24576 .. remove debugportmapper
    -       0 -       0 -       0 .. remove desktop_sessions
    -  815104 -    8192 -  544768 .. remove drive
    -   65536 -  356352 -  425984 .. remove kube
    -  233472 -  286720 -  311296 .. remove portmapper (and debugportmapper)
    -   90112 -       0 -  110592 .. remove relayserver
    -  655360 -  712704 -  598016 .. remove serve (and webclient)
    -  937984 -       0 -  950272 .. remove ssh
    -  708608 -  401408 -  344064 .. remove syspolicy
    -       0 - 4071424 -11132928 .. remove systray
    -  159744 -   61440 -  225280 .. remove taildrop
    -  618496 -  454656 -  757760 .. remove tailnetlock
    -  122880 -       0 -  131072 .. remove tap
    -  442368 -       0 -  483328 .. remove tpm
    -   16384 -       0 -   20480 .. remove wakeonlan
    -  278528 -  368640 -  286720 .. remove webclient

    Starting at a minimal binary and adding one feature back...

    tailscaled tailscale combined (linux/amd64)
     19308728  10870968  20119736 omitting everything
    +  352256 +  454656 +  643072 .. add acme
    + 2035712 +       0 + 2035712 .. add aws
    +    8192 +       0 +    8192 .. add bird
    +   20480 +   12288 +   36864 .. add capture
    +       0 +   57344 +   61440 .. add completion
    +  262144 +  274432 +  266240 .. add debugeventbus
    +  344064 +  118784 +  360448 .. add debugportmapper (and portmapper)
    +       0 +       0 +       0 .. add desktop_sessions
    +  978944 +    8192 +  991232 .. add drive
    +   61440 +  364544 +  425984 .. add kube
    +  331776 +  110592 +  335872 .. add portmapper
    +  122880 +       0 +  102400 .. add relayserver
    +  598016 +  155648 +  737280 .. add serve
    + 1142784 +       0 + 1142784 .. add ssh
    +  708608 +  860160 +  720896 .. add syspolicy
    +       0 + 4079616 + 6221824 .. add systray
    +  180224 +   65536 +  237568 .. add taildrop
    +  647168 +  393216 +  720896 .. add tailnetlock
    +  122880 +       0 +  126976 .. add tap
    +  446464 +       0 +  454656 .. add tpm
    +   20480 +       0 +   24576 .. add wakeonlan
    + 1011712 + 1011712 + 1138688 .. add webclient (and serve)

Fixes #17139

Change-Id: Ia91be2da00de8481a893243d577d20e988a0920a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-09-17 09:03:17 -07:00
committed by Brad Fitzpatrick
parent 4f211ea5c5
commit 78035fb9d2
4 changed files with 300 additions and 63 deletions
+80 -25
View File
@@ -4,6 +4,8 @@
// The featuretags package is a registry of all the ts_omit-able build tags.
package featuretags
import "tailscale.com/util/set"
// CLI is a special feature in the [Features] map that works opposite
// from the others: it is opt-in, rather than opt-out, having a different
// build tag format.
@@ -32,37 +34,90 @@ func (ft FeatureTag) OmitTag() string {
return "ts_omit_" + string(ft)
}
// Requires returns the set of features that must be included to
// use the given feature, including the provided feature itself.
func Requires(ft FeatureTag) set.Set[FeatureTag] {
s := set.Set[FeatureTag]{}
var add func(FeatureTag)
add = func(ft FeatureTag) {
if !ft.IsOmittable() {
return
}
s.Add(ft)
for _, dep := range Features[ft].Deps {
add(dep)
}
}
add(ft)
return s
}
// RequiredBy is the inverse of Requires: it returns the set of features that
// depend on the given feature (directly or indirectly), including the feature
// itself.
func RequiredBy(ft FeatureTag) set.Set[FeatureTag] {
s := set.Set[FeatureTag]{}
for f := range Features {
if featureDependsOn(f, ft) {
s.Add(f)
}
}
return s
}
// featureDependsOn reports whether feature a (directly or indirectly) depends on b.
// It returns true if a == b.
func featureDependsOn(a, b FeatureTag) bool {
if a == b {
return true
}
for _, dep := range Features[a].Deps {
if featureDependsOn(dep, b) {
return true
}
}
return false
}
// FeatureMeta describes a modular feature that can be conditionally linked into
// the binary.
type FeatureMeta struct {
Sym string // exported Go symbol for boolean const
Desc string // human-readable description
Sym string // exported Go symbol for boolean const
Desc string // human-readable description
Deps []FeatureTag // other features this feature requires
}
// Features are the known Tailscale features that can be selectively included or
// excluded via build tags, and a description of each.
var Features = map[FeatureTag]FeatureMeta{
"acme": {"ACME", "ACME TLS certificate management"},
"aws": {"AWS", "AWS integration"},
"bird": {"Bird", "Bird BGP integration"},
"capture": {"Capture", "Packet capture"},
"cli": {"CLI", "embed the CLI into the tailscaled binary"},
"completion": {"Completion", "CLI shell completion"},
"debugeventbus": {"DebugEventBus", "eventbus debug support"},
"debugportmapper": {"DebugPortMapper", "portmapper debug support"},
"desktop_sessions": {"DesktopSessions", "Desktop sessions support"},
"drive": {"Drive", "Tailscale Drive (file server) support"},
"kube": {"Kube", "Kubernetes integration"},
"portmapper": {"PortMapper", "NAT-PMP/PCP/UPnP port mapping support"},
"relayserver": {"RelayServer", "Relay server"},
"serve": {"Serve", "Serve and Funnel support"},
"ssh": {"SSH", "Tailscale SSH support"},
"syspolicy": {"SystemPolicy", "System policy configuration (MDM) support"},
"systray": {"SysTray", "Linux system tray"},
"taildrop": {"Taildrop", "Taildrop (file sending) support"},
"tailnetlock": {"TailnetLock", "Tailnet Lock support"},
"tap": {"Tap", "Experimental Layer 2 (ethernet) support"},
"tpm": {"TPM", "TPM support"},
"wakeonlan": {"WakeOnLAN", "Wake-on-LAN support"},
"webclient": {"WebClient", "Web client support"},
"acme": {"ACME", "ACME TLS certificate management", nil},
"aws": {"AWS", "AWS integration", nil},
"bird": {"Bird", "Bird BGP integration", nil},
"capture": {"Capture", "Packet capture", nil},
"cli": {"CLI", "embed the CLI into the tailscaled binary", nil},
"completion": {"Completion", "CLI shell completion", nil},
"debugeventbus": {"DebugEventBus", "eventbus debug support", nil},
"debugportmapper": {
Sym: "DebugPortMapper",
Desc: "portmapper debug support",
Deps: []FeatureTag{"portmapper"},
},
"desktop_sessions": {"DesktopSessions", "Desktop sessions support", nil},
"drive": {"Drive", "Tailscale Drive (file server) support", nil},
"kube": {"Kube", "Kubernetes integration", nil},
"portmapper": {"PortMapper", "NAT-PMP/PCP/UPnP port mapping support", nil},
"relayserver": {"RelayServer", "Relay server", nil},
"serve": {"Serve", "Serve and Funnel support", nil},
"ssh": {"SSH", "Tailscale SSH support", nil},
"syspolicy": {"SystemPolicy", "System policy configuration (MDM) support", nil},
"systray": {"SysTray", "Linux system tray", nil},
"taildrop": {"Taildrop", "Taildrop (file sending) support", nil},
"tailnetlock": {"TailnetLock", "Tailnet Lock support", nil},
"tap": {"Tap", "Experimental Layer 2 (ethernet) support", nil},
"tpm": {"TPM", "TPM support", nil},
"wakeonlan": {"WakeOnLAN", "Wake-on-LAN support", nil},
"webclient": {
Sym: "WebClient", Desc: "Web client support",
Deps: []FeatureTag{"serve"},
},
}
+81
View File
@@ -0,0 +1,81 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package featuretags
import (
"maps"
"slices"
"testing"
"tailscale.com/util/set"
)
func TestRequires(t *testing.T) {
for tag, meta := range Features {
for _, dep := range meta.Deps {
if _, ok := Features[dep]; !ok {
t.Errorf("feature %q has unknown dependency %q", tag, dep)
}
}
// And indirectly check for cycles. If there were a cycle,
// this would infinitely loop.
deps := Requires(tag)
t.Logf("deps of %q: %v", tag, slices.Sorted(maps.Keys(deps)))
}
}
func TestDepSet(t *testing.T) {
var setOf = set.Of[FeatureTag]
tests := []struct {
in FeatureTag
want set.Set[FeatureTag]
}{
{
in: "drive",
want: setOf("drive"),
},
{
in: "serve",
want: setOf("serve"),
},
{
in: "webclient",
want: setOf("webclient", "serve"),
},
}
for _, tt := range tests {
got := Requires(tt.in)
if !maps.Equal(got, tt.want) {
t.Errorf("DepSet(%q) = %v, want %v", tt.in, got, tt.want)
}
}
}
func TestRequiredBy(t *testing.T) {
var setOf = set.Of[FeatureTag]
tests := []struct {
in FeatureTag
want set.Set[FeatureTag]
}{
{
in: "drive",
want: setOf("drive"),
},
{
in: "webclient",
want: setOf("webclient"),
},
{
in: "serve",
want: setOf("webclient", "serve"),
},
}
for _, tt := range tests {
got := RequiredBy(tt.in)
if !maps.Equal(got, tt.want) {
t.Errorf("FeaturesWhichDependOn(%q) = %v, want %v", tt.in, got, tt.want)
}
}
}