Compare commits

...

4 Commits

Author SHA1 Message Date
David Anderson dbd2148b94 Dockerfile: fix docker build
The stamp vars got renamed and I forgot to update these scripts.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-13 16:56:22 -08:00
David Anderson 5ac12a3e4d version/mkversion: package to calculate version info for builds
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-12 14:35:23 -08:00
David Anderson db6b3f6b43 .github/workflows: add armv5 and armv7 cross tests
armv5 because that's what we ship to most downstreams right now,
armv7 becuase that's what we want to ship more of.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-11 18:31:50 -08:00
David Anderson d107f24c42 .github/workflows: simplify build-only go test invocation
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-11 18:12:51 -08:00
6 changed files with 668 additions and 14 deletions

View File

@ -144,6 +144,12 @@ jobs:
goarch: "386" # thanks yaml
- goos: linux
goarch: loong64
- goos: linux
goarch: arm
goarm: "5"
- goos: linux
goarch: arm
goarm: "7"
# macOS
- goos: darwin
goarch: amd64
@ -169,13 +175,10 @@ jobs:
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
CGO_ENABLED: "0"
- name: build tests
run: |
toolgo=`pwd`/tool/go
for d in $($toolgo list -f '{{if .TestGoFiles}}{{.Dir}}{{end}}' ./... ); do
(echo $d; cd $d && $toolgo test -c)
done
run: ./tool/go test -exec=true ./...
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}

View File

@ -62,9 +62,9 @@ ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
ARG TARGETARCH
RUN GOARCH=$TARGETARCH go install -ldflags="\
-X tailscale.com/version.long=$VERSION_LONG \
-X tailscale.com/version.short=$VERSION_SHORT \
-X tailscale.com/version.gitCommit=$VERSION_GIT_HASH" \
-X tailscale.com/version.longStamp=$VERSION_LONG \
-X tailscale.com/version.shortStamp=$VERSION_SHORT \
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
-v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot
FROM alpine:3.16

View File

@ -43,9 +43,9 @@ case "$TARGET" in
tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled, \
tailscale.com/cmd/containerboot:/usr/local/bin/containerboot" \
--ldflags="\
-X tailscale.com/version.long=${VERSION_LONG} \
-X tailscale.com/version.short=${VERSION_SHORT} \
-X tailscale.com/version.gitCommit=${VERSION_GIT_HASH}" \
-X tailscale.com/version.longStamp=${VERSION_LONG} \
-X tailscale.com/version.shortStamp=${VERSION_SHORT} \
-X tailscale.com/version.gitCommitStamp=${VERSION_GIT_HASH}" \
--base="${BASE}" \
--tags="${TAGS}" \
--repos="${REPOS}" \
@ -58,9 +58,9 @@ case "$TARGET" in
go run github.com/tailscale/mkctr \
--gopaths="tailscale.com/cmd/k8s-operator:/usr/local/bin/operator" \
--ldflags="\
-X tailscale.com/version.long=${VERSION_LONG} \
-X tailscale.com/version.short=${VERSION_SHORT} \
-X tailscale.com/version.gitCommit=${VERSION_GIT_HASH}" \
-X tailscale.com/version.longStamp=${VERSION_LONG} \
-X tailscale.com/version.shortStamp=${VERSION_SHORT} \
-X tailscale.com/version.gitCommitStamp=${VERSION_GIT_HASH}" \
--base="${BASE}" \
--tags="${TAGS}" \
--repos="${REPOS}" \

View File

@ -0,0 +1,36 @@
// mkversion gets version info from git and outputs a bunch of shell
// variables that get used elsewhere in the redo build system to embed
// version numbers into binaries.
//go:build ignore
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"tailscale.io/version"
)
func main() {
prefix := ""
if len(os.Args) > 1 {
if os.Args[1] == "--export" {
prefix = "export "
} else {
fmt.Println("usage: mkversion [--export|-h|--help]")
os.Exit(1)
}
}
var b bytes.Buffer
io.WriteString(&b, version.Info().String())
s := bufio.NewScanner(&b)
for s.Scan() {
fmt.Println(prefix + s.Text())
}
}

View File

@ -0,0 +1,442 @@
// The version package gets version info from git and provides a bunch
// of differently formatted version strings get used elsewhere in the
// build system to embed version numbers into binaries.
package version
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"tailscale.com/tailcfg"
)
// VersionInfo is all the version and related metadata we embed into binaries at
// build time.
type VersionInfo struct {
// Short is the short version string, like "1.2.3". It is what
// version.Short() returns.
Short string
// Long is the long version string, like "1.2.3-0-gabcdef123456". It is what
// version.Long() returns.
Long string
// GitCommit is the git commit hash of the tailscale/tailscale repository.
GitCommit string
// OtherCommit is the git commit hash of another repository used in the
// build. The exact other repository depends on what is being built, but
// could be for example tailscale/tailscale-android.
OtherCommit string
// Xcode is like Short, but with a much larger major version number.
//
// This exists because Xcode enforces monotonically increasing app versions,
// and early Tailscale app releases used a single incrementing number. When
// we transitioned to major.minor.patch format, we were forced to use a much
// higher major number to keep the versions sequential.
//
// This version number is used for the app metadata of the iOS and macsys
// (aka "standalone version" on pkgs.tailscale.com) apps.
Xcode string // For embedding into Xcode metadata (iOS and macsys)
// XcodeMacOS is like Xcode, but for the macOS app store app.
//
// For unclear reasons, at some point around Tailscale 1.15, our macOS app
// build stopped embedding Info.Xcode as the app version, and reverted to
// apple-managed sequentially increasing ints. Then, around 1.36, it stopped
// auto-incrementing those numbers, and we needed to do our own embedding
// again, at a version higher than the highest apple-generated number (273).
//
// So, we switched to embedding a version based on the timestamp of the
// commit being built. This version, like Info.Xcode, is never shown to
// users outside of TestFlight, so it should hopefully not be confusing to
// anyone but Tailscale devs.
XcodeMacOS string
// Winres is like Short, but formatted for use in Windows resource files
// (.rc). This is what populates the "Product Version" field when you
// right-click->Properties on a Tailscale executable.
Winres string // For embedding into Windows metadata
// Track is the release track of the build: "stable" for even minor
// versions, and "unstable" for odd minor versions.
Track string
// MSIProductCodes is a map of Windows CPU architecture names to a v5 UUID
// for the corresponding build. The UUIDs are unique and deterministic for a
// unique major.minor.patch and CPU architecture.
//
// As the name suggests, these UUIDs get embedded into Tailscale's Windows
// MSI files. See
// https://learn.microsoft.com/en-us/windows/win32/msi/product-codes for
// more information.
MSIProductCodes map[string]string
// Copyright is a Tailscale copyright string, stamped with the year in which
// Info was generated. It gets embedded into Apple app metadata.
Copyright string
// CapabilityVersion is the capability version of the control protocol. See
// tailscale.com/tailcfg.CurrentCapabilityVersion for more information.
//
// The version is mirrored from tailcfg into this struct so that it can be
// exposed to non-Go languages that some of our builds interface with (e.g.
// Swift for Apple builds).
CapabilityVersion int
}
// String returns v as a series of shell variable assignments
// ("VERSION_SHORT=...").
func (v VersionInfo) String() string {
return v.export("")
}
// Export returns v as a series of shell variable exports ("export
// VERSION_SHORT=...").
func (v VersionInfo) Export() string {
return v.export("export ")
}
func (v VersionInfo) export(prefix string) string {
var b bytes.Buffer
f := func(format string, args ...any) {
fmt.Fprintf(&b, prefix+format, args...)
}
f("VERSION_SHORT=%q\n", v.Short)
f("VERSION_LONG=%q\n", v.Long)
f("VERSION_GIT_HASH=%q\n", v.GitCommit)
f("VERSION_EXTRA_HASH=%q\n", v.OtherCommit)
f("VERSION_XCODE=%q\n", v.Xcode)
f("VERSION_XCODE_MACOS=%q\n", v.XcodeMacOS)
f("VERSION_WINRES=%q\n", v.Winres)
f("VERSION_TRACK=%q\n", v.Track)
// Ensure a predictable order for these variables for testing purposes.
keys := make([]string, 0, len(v.MSIProductCodes))
for k := range v.MSIProductCodes {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
f("VERSION_MSIPRODUCT_%s=%q\n", strings.ToUpper(k), v.MSIProductCodes[k])
}
fmt.Fprintf(&b, "VERSION_COPYRIGHT=%q\n", v.Copyright)
fmt.Fprintf(&b, "VERSION_CAPABILITY=%d\n", v.CapabilityVersion)
return b.String()
}
// Info returns a VersionInfo from dir. dir must be within a git checkout,
// either of the tailscale.com Go module or a Go module that imports the
// tailscale.com module.
func Info(dir string) (VersionInfo, error) {
runner := dirRunner(dir)
repoRoot, err := runner.output("git", "rev-parse", "--show-toplevel")
if err != nil {
return VersionInfo{}, fmt.Errorf("couldn't find git repo root: %w", err)
}
runner = dirRunner(repoRoot)
goTool := filepath.Join(repoRoot, "tool/go")
if _, err := os.Stat(goTool); errors.Is(err, os.ErrNotExist) {
// Fall back to $PATH lookup and hope that Go version is recent enough
// to handle our go.mod.
goTool = "go"
} else if err != nil {
return VersionInfo{}, fmt.Errorf("looking for %s: %w", goTool, err)
}
// Find the tailscale.com module, which may or may not be repoRoot.
tailscaleDir, tailscaleCommit, err := locateTailscaleModule(runner, goTool)
if err != nil {
return VersionInfo{}, err
}
trunner := dirRunner(tailscaleDir)
baseCommit, err := trunner.output("git", "rev-list", "--max-count=1", tailscaleCommit, "--", "VERSION.txt")
if err != nil {
return VersionInfo{}, fmt.Errorf("getting tailscale.com release base commit: %w", err)
}
baseVersion, err := trunner.output("git", "show", baseCommit+":VERSION.txt")
if err != nil {
return VersionInfo{}, fmt.Errorf("getting tailscale.com release base version: %w", err)
}
major, minor, patch, err := parseVersion(baseVersion)
if err != nil {
return VersionInfo{}, fmt.Errorf("parsing tailscale.com release base version: %w", err)
}
s, err := trunner.output("git", "rev-list", "--count", tailscaleCommit, "^"+baseCommit)
if err != nil {
return VersionInfo{}, fmt.Errorf("getting tailscale.com release change count: %w", err)
}
changeCount, err := strconv.Atoi(s)
if err != nil {
return VersionInfo{}, fmt.Errorf("parsing tailscale.com release change count: %w", err)
}
v := verInfo{
major: major,
minor: minor,
patch: patch,
changeCount: changeCount,
commit: tailscaleCommit,
}
if !trunner.ok("git", "diff-index", "--quiet", "HEAD") {
v.dirty = true
}
var ts string
if tailscaleDir != repoRoot {
// Building from a different repo that imports tailscale.com, grab its
// info as well.
v.otherCommit, err = runner.output("git", "rev-parse", "HEAD")
if err != nil {
return VersionInfo{}, err
}
if !runner.ok("git", "diff-index", "--quiet", "HEAD") {
v.dirty = true
}
ts, err = runner.output("git", "log", "-n1", "--format=%ct", v.otherCommit)
if err != nil {
return VersionInfo{}, fmt.Errorf("getting commit timestamp of %q: %w", v.otherCommit, err)
}
} else {
ts, err = trunner.output("git", "log", "-n1", "--format=%ct", v.commit)
if err != nil {
return VersionInfo{}, fmt.Errorf("getting commit timestamp of %q: %w", v.commit, err)
}
}
tsInt, err := strconv.ParseInt(ts, 10, 64)
if err != nil {
return VersionInfo{}, fmt.Errorf("parsing commit timestamp %q: %w", ts, err)
}
v.timestamp = time.Unix(tsInt, 0).UTC()
return mkOutput(v)
}
func mkOutput(v verInfo) (VersionInfo, error) {
var (
changeSuffix string
track string
)
if v.minor%2 == 1 {
// Odd minor numbers are unstable builds.
if v.patch != 0 {
return VersionInfo{}, fmt.Errorf("unstable release %d.%d.%d has a non-zero patch number, which is not allowed", v.major, v.minor, v.patch)
}
track = "unstable"
v.patch, v.changeCount = v.changeCount, 0
} else {
track = "stable"
if v.changeCount != 0 {
// Even minor numbers are stable builds, but stable builds are
// supposed to have a zero change count. Therefore, we're currently
// describing a commit that's on a release branch, but hasn't been
// tagged as a patch release yet.
//
// We used to change the version number to 0.0.0 in that case, but that
// caused some features to get disabled due to the low version number.
// Instead, add yet another suffix to the version number, with a change
// count.
changeSuffix = "-" + strconv.Itoa(v.changeCount)
}
}
var hashes string
if v.otherCommit != "" {
hashes = "-g" + shortHash(v.otherCommit)
}
if v.commit != "" {
hashes = "-t" + shortHash(v.commit) + hashes
}
// Generate a monotonically increasing version number for the macOS app, as
// expected by Apple. We use the date so that it's always increasing (if we
// based it on the actual version number we'd run into issues when doing
// cherrypick stable builds from a release branch after unstable builds from
// HEAD).
//
// We started to need to do this in 2023, and the last Apple-generated
// incrementing build number was 273. To avoid using up the space, we
// use <year - 1750> as the major version (thus 273.*, 274.* in 2024, etc.),
// so that we we're still in the same range. This way if Apple goes back to
// auto-incrementing the number for us, we can go back to it with
// reasonable-looking numbers.
xcodeMacOS := fmt.Sprintf("%d.%d.%d", v.timestamp.Year()-1750, v.timestamp.YearDay(), v.timestamp.Hour()*60*60+v.timestamp.Minute()*60+v.timestamp.Second())
return VersionInfo{
Short: fmt.Sprintf("%d.%d.%d", v.major, v.minor, v.patch),
Long: fmt.Sprintf("%d.%d.%d%s%s", v.major, v.minor, v.patch, changeSuffix, hashes),
GitCommit: v.commit,
OtherCommit: v.otherCommit,
Xcode: fmt.Sprintf("%d.%d.%d", v.major+100, v.minor, v.patch),
XcodeMacOS: xcodeMacOS,
Winres: fmt.Sprintf("%d,%d,%d,0", v.major, v.minor, v.patch),
Track: track,
MSIProductCodes: makeMSIProductCodes(v, track),
Copyright: fmt.Sprintf("Copyright © %d Tailscale Inc. All Rights Reserved.", time.Now().Year()),
CapabilityVersion: int(tailcfg.CurrentCapabilityVersion),
}, nil
}
// makeMSIProductCodes produces per-architecture v5 UUIDs derived from the pkgs
// url that would be used for the current version, thus ensuring that product IDs
// are mapped 1:1 to a unique version number.
func makeMSIProductCodes(v verInfo, track string) map[string]string {
urlBase := fmt.Sprintf("https://pkgs.tailscale.com/%s/tailscale-setup-%d.%d.%d-", track, v.major, v.minor, v.patch)
ret := map[string]string{}
for _, arch := range []string{"amd64", "arm64", "x86"} {
url := fmt.Sprintf("%s%s.msi", urlBase, arch)
curUUID := uuid.NewSHA1(uuid.NameSpaceURL, []byte(url))
// MSI prefers hex digits in UUIDs to be uppercase.
ret[arch] = strings.ToUpper(curUUID.String())
}
return ret
}
// locateTailscaleModule returns the directory of a git checkout of the
// tailscale.com Go module, and the commit hash from which to build from.
//
// If necessary, locateTailscaleModule fetches a git clone of the tailscale.com
// repository into a cache dir.
func locateTailscaleModule(runner dirRunner, goTool string) (dir, commit string, err error) {
modDir, err := runner.output(goTool, "list", "-m", "-f", "{{.Dir}}", "tailscale.com")
if err != nil {
return "", "", fmt.Errorf("getting tailscale.com module dir: %w", err)
}
if modDir != "" {
ok, err := exists(filepath.Join(modDir, ".git"))
if err != nil {
return "", "", fmt.Errorf("checking for .git in %q: %w", modDir, err)
}
if ok {
commit, err := dirRunner(modDir).output("git", "rev-parse", "HEAD")
if err != nil {
return "", "", fmt.Errorf("getting git commit in %q: %w", modDir, err)
}
return modDir, commit, nil
}
// Otherwise, fall through, we have to fetch a git clone.
}
commit, err = runner.output(goTool, "list", "-m", "-f", "{{.Version}}", "tailscale.com")
if err != nil {
return "", "", fmt.Errorf("getting tailscale.com module version: %w", err)
}
// Last dash-separated portion of version is a commit hash.
commit = commit[strings.LastIndex(commit, "-")+1:]
cacheDir, err := os.UserCacheDir()
if err != nil {
return "", "", fmt.Errorf("finding user cache dir: %w", err)
}
tailscaleCache := filepath.Join(cacheDir, "tailscale-oss")
ok, err := exists(tailscaleCache)
if err != nil {
return "", "", fmt.Errorf("checking for tailscale cache dir: %w", err)
}
if !ok {
if !runner.ok("git", "clone", "https://github.com/tailscale/tailscale", tailscaleCache) {
return "", "", fmt.Errorf("cloning tailscale repo failed")
}
}
r := dirRunner(tailscaleCache)
if !r.ok("git", "cat-file", "-e", commit) {
if !r.ok("git", "fetch", "origin") {
return "", "", fmt.Errorf("updating cached tailscale repo failed")
}
if !r.ok("git", "cat-file", "-e", commit) {
return "", "", fmt.Errorf("commit %q not found in tailscale repo after fetch", commit)
}
}
// Expand the commit to its full form.
commit, err = r.output("git", "rev-parse", commit)
if err != nil {
return "", "", fmt.Errorf("expanding commit %q: %w", commit, err)
}
return tailscaleCache, commit, nil
}
type verInfo struct {
major, minor, patch int
changeCount int
commit string
otherCommit string
dirty bool // either commit or otherCommit is in a dirty repo
timestamp time.Time // of otherCommit if present, otherwise of commit
}
func parseVersion(s string) (major, minor, patch int, err error) {
fs := strings.Split(s, ".")
if len(fs) != 3 {
err = fmt.Errorf("parseVersion: parsing %q: wrong number of parts: %d", s, len(fs))
return
}
ints := make([]int, 0, 3)
for _, s := range fs {
var i int
i, err = strconv.Atoi(s)
if err != nil {
err = fmt.Errorf("parseVersion: parsing %q: %w", s, err)
return
}
ints = append(ints, i)
}
return ints[0], ints[1], ints[2], nil
}
func shortHash(hash string) string {
if len(hash) < 9 {
return hash
}
return hash[:9]
}
// dirRunner executes commands in the specified dir.
type dirRunner string
func (r dirRunner) output(prog string, args ...string) (string, error) {
cmd := exec.Command(prog, args...)
// Sometimes, our binaries end up running in a world where GO111MODULE=off,
// because x/tools/go/packages disables Go modules on occasion and then runs
// other Go code. This breaks executing "go mod edit", which requires that
// Go modules be enabled.
//
// Since nothing we do here ever wants Go modules to be turned off, force it
// on here so that we can read module data regardless of the environment.
//
// Similarly, our internal build system (gocross) uses this code to generate
// version numbers for embedding, so we have to bypass it here in order to
// avoid an infinite recursion.
cmd.Env = append(os.Environ(), "GO111MODULE=on", "GOCROSS_BYPASS=1")
cmd.Dir = string(r)
out, err := cmd.Output()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("running %v: %w, out=%s, err=%s", cmd.Args, err, out, ee.Stderr)
}
return "", fmt.Errorf("running %v: %w, %s", cmd.Args, err, out)
}
return strings.TrimSpace(string(out)), nil
}
func (r dirRunner) ok(prog string, args ...string) bool {
cmd := exec.Command(prog, args...)
cmd.Dir = string(r)
return cmd.Run() == nil
}
func exists(path string) (ok bool, err error) {
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return false, nil
} else if err != nil {
return false, err
}
return true, nil
}

View File

@ -0,0 +1,173 @@
package version
import (
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
func mkInfo(gitCommit, otherCommit string, timestamp time.Time, major, minor, patch, changeCount int) verInfo {
return verInfo{
major: major,
minor: minor,
patch: patch,
changeCount: changeCount,
commit: gitCommit,
otherCommit: otherCommit,
timestamp: timestamp,
}
}
func TestMkversion(t *testing.T) {
corpDate := time.Date(2023, time.January, 27, 1, 2, 3, 4, time.UTC)
tests := []struct {
in verInfo
want string
}{
{mkInfo("abcdef", "", corpDate, 0, 98, 0, 0), `
VERSION_SHORT="0.98.0"
VERSION_LONG="0.98.0-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="100.98.0"
VERSION_XCODE_MACOS="273.27.3723"
VERSION_WINRES="0,98,0,0"
VERSION_TRACK="stable"
VERSION_MSIPRODUCT_AMD64="C653B075-AD91-5265-9DF8-0087D35D148D"
VERSION_MSIPRODUCT_ARM64="1C41380B-A742-5A3C-AF5D-DF7894DD0FB8"
VERSION_MSIPRODUCT_X86="4ABDDA14-7499-5C2E-A62A-DD435C50C4CB"
VERSION_COPYRIGHT="Placeholder"
VERSION_CAPABILITY=42`},
{mkInfo("abcdef", "", corpDate, 0, 98, 1, 0), `
VERSION_SHORT="0.98.1"
VERSION_LONG="0.98.1-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="100.98.1"
VERSION_XCODE_MACOS="273.27.3723"
VERSION_WINRES="0,98,1,0"
VERSION_TRACK="stable"
VERSION_MSIPRODUCT_AMD64="DFD6DCF2-06D8-5D19-BDA0-FAF31E44EC23"
VERSION_MSIPRODUCT_ARM64="A4CCF19C-372B-5007-AFD8-1AF661DFF670"
VERSION_MSIPRODUCT_X86="FF12E937-DDC4-5868-9B63-D35B2050D4EA"
VERSION_COPYRIGHT="Placeholder"
VERSION_CAPABILITY=42`},
{mkInfo("abcdef", "", corpDate, 1, 2, 9, 0), `
VERSION_SHORT="1.2.9"
VERSION_LONG="1.2.9-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="101.2.9"
VERSION_XCODE_MACOS="273.27.3723"
VERSION_WINRES="1,2,9,0"
VERSION_TRACK="stable"
VERSION_MSIPRODUCT_AMD64="D47B5157-FF26-5A10-A94E-50E4529303A9"
VERSION_MSIPRODUCT_ARM64="91D16F75-2A12-5E12-820A-67B89BF858E7"
VERSION_MSIPRODUCT_X86="8F1AC1C6-B93B-5C70-802E-6AE9591FA0D6"
VERSION_COPYRIGHT="Placeholder"
VERSION_CAPABILITY=42`},
{mkInfo("abcdef", "", corpDate, 1, 15, 0, 129), `
VERSION_SHORT="1.15.129"
VERSION_LONG="1.15.129-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="101.15.129"
VERSION_XCODE_MACOS="273.27.3723"
VERSION_WINRES="1,15,129,0"
VERSION_TRACK="unstable"
VERSION_MSIPRODUCT_AMD64="89C96952-1FB8-5A4D-B02E-16A8060C56AA"
VERSION_MSIPRODUCT_ARM64="DB1A2E86-66C4-5CEC-8F4C-7DB805370F3A"
VERSION_MSIPRODUCT_X86="DC57C0C3-5164-5C92-86B3-2800CEFF0540"
VERSION_COPYRIGHT="Placeholder"
VERSION_CAPABILITY=42`},
{mkInfo("abcdef", "", corpDate, 1, 2, 0, 17), `
VERSION_SHORT="1.2.0"
VERSION_LONG="1.2.0-17-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="101.2.0"
VERSION_XCODE_MACOS="273.27.3723"
VERSION_WINRES="1,2,0,0"
VERSION_TRACK="stable"
VERSION_MSIPRODUCT_AMD64="0F9709AE-0E5E-51AF-BCCD-A25314B4CE8B"
VERSION_MSIPRODUCT_ARM64="39D5D46E-E644-5C80-9EF8-224AC1AD5969"
VERSION_MSIPRODUCT_X86="4487136B-2D11-5E42-BD80-B8529F3326F4"
VERSION_COPYRIGHT="Placeholder"
VERSION_CAPABILITY=42`},
{mkInfo("abcdef", "defghi", corpDate, 1, 15, 0, 129), `
VERSION_SHORT="1.15.129"
VERSION_LONG="1.15.129-tabcdef-gdefghi"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH="defghi"
VERSION_XCODE="101.15.129"
VERSION_XCODE_MACOS="273.27.3723"
VERSION_WINRES="1,15,129,0"
VERSION_TRACK="unstable"
VERSION_MSIPRODUCT_AMD64="89C96952-1FB8-5A4D-B02E-16A8060C56AA"
VERSION_MSIPRODUCT_ARM64="DB1A2E86-66C4-5CEC-8F4C-7DB805370F3A"
VERSION_MSIPRODUCT_X86="DC57C0C3-5164-5C92-86B3-2800CEFF0540"
VERSION_COPYRIGHT="Placeholder"
VERSION_CAPABILITY=42`},
{mkInfo("abcdef", "", corpDate, 1, 2, 0, 17), `
VERSION_SHORT="1.2.0"
VERSION_LONG="1.2.0-17-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="101.2.0"
VERSION_XCODE_MACOS="273.27.3723"
VERSION_WINRES="1,2,0,0"
VERSION_TRACK="stable"
VERSION_MSIPRODUCT_AMD64="0F9709AE-0E5E-51AF-BCCD-A25314B4CE8B"
VERSION_MSIPRODUCT_ARM64="39D5D46E-E644-5C80-9EF8-224AC1AD5969"
VERSION_MSIPRODUCT_X86="4487136B-2D11-5E42-BD80-B8529F3326F4"
VERSION_COPYRIGHT="Placeholder"
VERSION_CAPABILITY=42`},
{mkInfo("abcdef", "defghi", corpDate, 1, 15, 0, 129), `
VERSION_SHORT="1.15.129"
VERSION_LONG="1.15.129-tabcdef-gdefghi"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH="defghi"
VERSION_XCODE="101.15.129"
VERSION_XCODE_MACOS="273.27.3723"
VERSION_WINRES="1,15,129,0"
VERSION_TRACK="unstable"
VERSION_MSIPRODUCT_AMD64="89C96952-1FB8-5A4D-B02E-16A8060C56AA"
VERSION_MSIPRODUCT_ARM64="DB1A2E86-66C4-5CEC-8F4C-7DB805370F3A"
VERSION_MSIPRODUCT_X86="DC57C0C3-5164-5C92-86B3-2800CEFF0540"
VERSION_COPYRIGHT="Placeholder"
VERSION_CAPABILITY=42`},
{mkInfo("abcdef", "", corpDate, 0, 99, 5, 0), ""}, // unstable, patch number not allowed
{mkInfo("abcdef", "", corpDate, 0, 99, 5, 123), ""}, // unstable, patch number not allowed
{mkInfo("abcdef", "defghi", time.Time{}, 1, 15, 0, 129), ""}, // missing corpDate
}
for _, test := range tests {
want := strings.ReplaceAll(strings.TrimSpace(test.want), " ", "")
info, err := mkOutput(test.in)
if err != nil {
if test.want != "" {
t.Errorf("%#v got unexpected error %v", test.in, err)
}
continue
}
// Normalize some parts so the test outputs above aren't brittle.
if info.CapabilityVersion == 0 {
t.Errorf("info.CapabilityVersion not set")
}
info.CapabilityVersion = 42
if !strings.Contains(info.Copyright, "Copyright") || !strings.Contains(info.Copyright, "Tailscale") {
t.Errorf("info.Copyright not correct, got %q", info.Copyright)
}
info.Copyright = "Placeholder"
got := strings.TrimSpace(info.String())
if diff := cmp.Diff(got, want); want != "" && diff != "" {
t.Errorf("%#v wrong output (-got+want):\n%s", test.in, diff)
}
}
}