// Copyright (c) Tailscale Inc & contributors // SPDX-License-Identifier: BSD-3-Clause // Package wasmbuild contains the shared build flags and manifest layout // used to produce the @tailscale/connect NPM package's main.wasm. It is // imported both by cmd/tsconnect (which does the build) and by tests // that verify the produced pkg/main.wasm matches what the current // source tree would build (see tstest/integration/jswasmtest). package wasmbuild import ( "fmt" "os" "os/exec" "path/filepath" "runtime" "slices" "strings" "tailscale.com/feature/featuretags" ) // baseTags are the non-featuretag build tags always set for the wasm // build. Featuretag omits (ts_omit_*) are computed dynamically by // [Tags] from [Keep] using the [featuretags] registry. // // Note: nethttpomithttp2 is intentionally NOT included: control/ts2021 // (since commit 1d93bdce2, Oct 2025) requires HTTP/2 from net/http's // bundled implementation. Excluding it leaves the wasm client unable // to negotiate with any control plane. var baseTags = []string{ "tailscale_go", "osusergo", "netgo", "omitidna", "omitpemdecrypt", } // Omit is the set of feature/featuretags tags excluded from the // cmd/tsconnect/wasm build via their ts_omit_ build tag (computed by // [Tags]). Everything else in [featuretags.Features] stays linked. // // Upstream uses the opposite polarity here — a small allow-list — because // its wasm client is only an SSH/fetch-in-browser tool. This fork's JS // bridge exposes Taildrop, Taildrive, Funnel/serve, ACME certs, exit node // selection, service advertisement and the peerAPI, so an allow-list is // the wrong default: a missing entry is not a compile error, it is a // feature that silently stops working at runtime (an omitted extension // simply never registers its hooks). Linking everything also matches how // this build behaved before upstream introduced featuretags. // // The init() below panics if any entry is unknown to feature/featuretags, // so a rename / removal in that registry fails loudly here. // // Trimming the bundle by omitting more features is worthwhile but should // be done with measurements and per-feature runtime verification, not by // assuming a feature is unreachable from the browser. var Omit = []featuretags.FeatureTag{ // feature/ace does not compile for GOOS=js: control/controlhttp only // installs HookMakeACEDialer on non-js platforms, so feature/ace's // reference to it is undefined here. "ace", } func init() { for _, ft := range Omit { if _, ok := featuretags.Features[ft]; !ok { panic(fmt.Sprintf("wasmbuild.Omit references unknown feature tag %q; "+ "did feature/featuretags rename or remove it?", ft)) } } } // ProdLDFlags is the -ldflags value used in production wasm builds. // -s strips the symbol table, -w strips DWARF, both to shrink the // shipped artifact. const ProdLDFlags = "-s -w" // BuildInfoFile is the basename of the JSON manifest that build-pkg // writes alongside main.wasm, recording the sha256 of the raw // (pre-wasm-opt) go-build output. Tests use this to detect a stale // pkg/main.wasm without having to re-run wasm-opt themselves. const BuildInfoFile = "build-info.json" // BuildInfo is the JSON contents of [BuildInfoFile]. type BuildInfo struct { // RawWasmSHA256 is the lowercase hex sha256 of the wasm bytes as // they came out of `go build` (before wasm-opt was run in place). RawWasmSHA256 string `json:"raw_wasm_sha256"` } // Tags returns the joined -tags value for the wasm build: [baseTags] // plus a ts_omit_ for every entry in [Omit]. // // The result is sorted so that the same source tree always produces // the same string (and therefore the same wasm bytes, given identical // inputs to `go build`). func Tags() string { omit := map[featuretags.FeatureTag]bool{} for _, ft := range Omit { omit[ft] = true } tags := slices.Clone(baseTags) for ft := range featuretags.Features { if ft == "" || !ft.IsOmittable() { continue } if omit[ft] { tags = append(tags, ft.OmitTag()) } } slices.Sort(tags) return strings.Join(tags, ",") } // ProdCommand returns an *exec.Cmd that runs `go build` for // cmd/tsconnect/wasm with the production flags, writing the raw // (pre-wasm-opt) wasm to outputPath. GOOS=js GOARCH=wasm is set in the // command's environment. The caller is responsible for wiring // Stdin/Stdout/Stderr and invoking Run. // // If goBin is empty, runtime.GOROOT()+"/bin/go" is used so that the // build runs under the same toolchain that built the caller. func ProdCommand(goBin, outputPath string) *exec.Cmd { if goBin == "" { goBin = filepath.Join(runtime.GOROOT(), "bin", "go") } cmd := exec.Command(goBin, "build", "-tags", Tags(), "-trimpath", "-ldflags", ProdLDFlags, "-o", outputPath, "tailscale.com/cmd/tsconnect/wasm", ) cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm") return cmd }