Updates tailscale/corp#9221 Signed-off-by: David Anderson <danderson@tailscale.com>main
parent
44e027abca
commit
fc4b25d9fd
@ -0,0 +1,134 @@ |
||||
// The dist command builds Tailscale release packages for distribution.
|
||||
package main |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"flag" |
||||
"fmt" |
||||
"log" |
||||
"os" |
||||
"path/filepath" |
||||
"sort" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli" |
||||
"tailscale.com/release/dist" |
||||
"tailscale.com/release/dist/unixpkgs" |
||||
) |
||||
|
||||
func main() { |
||||
var targets []dist.Target |
||||
targets = append(targets, unixpkgs.Targets()...) |
||||
sort.Slice(targets, func(i, j int) bool { |
||||
return targets[i].String() < targets[j].String() |
||||
}) |
||||
|
||||
rootCmd := &ffcli.Command{ |
||||
Name: "dist", |
||||
ShortUsage: "dist [flags] <command> [command flags]", |
||||
ShortHelp: "Build tailscale release packages for distribution", |
||||
LongHelp: `For help on subcommands, add --help after: "dist list --help".`, |
||||
Subcommands: []*ffcli.Command{ |
||||
{ |
||||
Name: "list", |
||||
Exec: func(ctx context.Context, args []string) error { |
||||
return runList(ctx, args, targets) |
||||
}, |
||||
ShortUsage: "dist list [target filters]", |
||||
ShortHelp: "List all available release targets.", |
||||
LongHelp: strings.TrimSpace(` |
||||
If filters are provided, only targets matching at least one filter are listed. |
||||
Filters can use glob patterns (* and ?). |
||||
`), |
||||
}, |
||||
{ |
||||
Name: "build", |
||||
Exec: func(ctx context.Context, args []string) error { |
||||
return runBuild(ctx, args, targets) |
||||
}, |
||||
ShortUsage: "dist build [target filters]", |
||||
ShortHelp: "Build release files", |
||||
FlagSet: (func() *flag.FlagSet { |
||||
fs := flag.NewFlagSet("build", flag.ExitOnError) |
||||
fs.StringVar(&buildArgs.manifest, "manifest", "", "manifest file to write") |
||||
return fs |
||||
})(), |
||||
LongHelp: strings.TrimSpace(` |
||||
If filters are provided, only targets matching at least one filter are built. |
||||
Filters can use glob patterns (* and ?). |
||||
`), |
||||
}, |
||||
}, |
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp }, |
||||
} |
||||
|
||||
if err := rootCmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil && !errors.Is(err, flag.ErrHelp) { |
||||
log.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
func runList(ctx context.Context, filters []string, targets []dist.Target) error { |
||||
tgts, err := dist.FilterTargets(targets, filters) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
for _, tgt := range tgts { |
||||
fmt.Println(tgt) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
var buildArgs struct { |
||||
manifest string |
||||
} |
||||
|
||||
func runBuild(ctx context.Context, filters []string, targets []dist.Target) error { |
||||
tgts, err := dist.FilterTargets(targets, filters) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if len(tgts) == 0 { |
||||
return errors.New("no targets matched (did you mean 'dist build all'?)") |
||||
} |
||||
|
||||
st := time.Now() |
||||
wd, err := os.Getwd() |
||||
if err != nil { |
||||
return fmt.Errorf("getting working directory: %w", err) |
||||
} |
||||
b, err := dist.NewBuild(wd, filepath.Join(wd, "dist")) |
||||
if err != nil { |
||||
return fmt.Errorf("creating build context: %w", err) |
||||
} |
||||
defer b.Close() |
||||
|
||||
out, err := b.Build(tgts) |
||||
if err != nil { |
||||
return fmt.Errorf("building targets: %w", err) |
||||
} |
||||
|
||||
if buildArgs.manifest != "" { |
||||
// Make the built paths relative to the manifest file.
|
||||
manifest, err := filepath.Abs(buildArgs.manifest) |
||||
if err != nil { |
||||
return fmt.Errorf("getting absolute path of manifest: %w", err) |
||||
} |
||||
fmt.Println(manifest) |
||||
fmt.Println(filepath.Join(b.Out, out[0])) |
||||
for i := range out { |
||||
rel, err := filepath.Rel(filepath.Dir(manifest), filepath.Join(b.Out, out[i])) |
||||
if err != nil { |
||||
return fmt.Errorf("making path relative: %w", err) |
||||
} |
||||
out[i] = rel |
||||
} |
||||
if err := os.WriteFile(manifest, []byte(strings.Join(out, "\n")), 0644); err != nil { |
||||
return fmt.Errorf("writing manifest: %w", err) |
||||
} |
||||
} |
||||
|
||||
fmt.Println("Done! Took", time.Since(st)) |
||||
return nil |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then |
||||
deb-systemd-helper unmask 'tailscaled.service' >/dev/null || true |
||||
if deb-systemd-helper --quiet was-enabled 'tailscaled.service'; then |
||||
deb-systemd-helper enable 'tailscaled.service' >/dev/null || true |
||||
else |
||||
deb-systemd-helper update-state 'tailscaled.service' >/dev/null || true |
||||
fi |
||||
|
||||
if [ -d /run/systemd/system ]; then |
||||
systemctl --system daemon-reload >/dev/null || true |
||||
deb-systemd-invoke restart 'tailscaled.service' >/dev/null || true |
||||
fi |
||||
fi |
||||
@ -0,0 +1,17 @@ |
||||
#!/bin/sh |
||||
set -e |
||||
if [ -d /run/systemd/system ] ; then |
||||
systemctl --system daemon-reload >/dev/null || true |
||||
fi |
||||
|
||||
if [ -x "/usr/bin/deb-systemd-helper" ]; then |
||||
if [ "$1" = "remove" ]; then |
||||
deb-systemd-helper mask 'tailscaled.service' >/dev/null || true |
||||
fi |
||||
|
||||
if [ "$1" = "purge" ]; then |
||||
deb-systemd-helper purge 'tailscaled.service' >/dev/null || true |
||||
deb-systemd-helper unmask 'tailscaled.service' >/dev/null || true |
||||
rm -rf /var/lib/tailscale |
||||
fi |
||||
fi |
||||
@ -0,0 +1,7 @@ |
||||
#!/bin/sh |
||||
set -e |
||||
if [ "$1" = "remove" ]; then |
||||
if [ -d /run/systemd/system ]; then |
||||
deb-systemd-invoke stop 'tailscaled.service' >/dev/null || true |
||||
fi |
||||
fi |
||||
@ -0,0 +1,268 @@ |
||||
// Package dist is a release artifact builder library.
|
||||
package dist |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"log" |
||||
"os" |
||||
"os/exec" |
||||
"path/filepath" |
||||
"regexp" |
||||
"runtime" |
||||
"sort" |
||||
"strings" |
||||
"sync" |
||||
|
||||
"tailscale.com/util/multierr" |
||||
"tailscale.com/version/mkversion" |
||||
) |
||||
|
||||
// A Target is something that can be build in a Build.
|
||||
type Target interface { |
||||
String() string |
||||
Build(build *Build) ([]string, error) |
||||
} |
||||
|
||||
// A Build is a build context for Targets.
|
||||
type Build struct { |
||||
// Repo is a path to the root Go module for the build.
|
||||
Repo string |
||||
// Tmp is a temporary directory that gets deleted when the Builder is closed.
|
||||
Tmp string |
||||
// Out is where build artifacts are written.
|
||||
Out string |
||||
// Go is the path to the Go binary to use for building.
|
||||
Go string |
||||
// Version is the version info of the build.
|
||||
Version mkversion.VersionInfo |
||||
|
||||
// once is a cache of function invocations that should run once per process
|
||||
// (for example building a helper docker container)
|
||||
once once |
||||
|
||||
extraMu sync.Mutex |
||||
extra map[any]any |
||||
|
||||
goBuilds Memoize[string] |
||||
// When running `dist build all` on a cold Go build cache, the fanout of
|
||||
// gooses and goarches results in a very large number of compile processes,
|
||||
// which bogs down the build machine.
|
||||
//
|
||||
// This throttles the number of concurrent `go build` invocations to the
|
||||
// number of CPU cores, which empirically keeps the builder responsive
|
||||
// without impacting overall build time.
|
||||
goBuildLimit chan struct{} |
||||
} |
||||
|
||||
// NewBuild creates a new Build rooted at repo, and writing artifacts to out.
|
||||
func NewBuild(repo, out string) (*Build, error) { |
||||
if err := os.MkdirAll(out, 0750); err != nil { |
||||
return nil, fmt.Errorf("creating out dir: %w", err) |
||||
} |
||||
tmp, err := os.MkdirTemp("", "dist-*") |
||||
if err != nil { |
||||
return nil, fmt.Errorf("creating tempdir: %w", err) |
||||
} |
||||
repo, err = findModRoot(repo) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("finding module root: %w", err) |
||||
} |
||||
goTool, err := findGo(repo) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("finding go binary: %w", err) |
||||
} |
||||
b := &Build{ |
||||
Repo: repo, |
||||
Tmp: tmp, |
||||
Out: out, |
||||
Go: goTool, |
||||
Version: mkversion.Info(), |
||||
extra: map[any]any{}, |
||||
goBuildLimit: make(chan struct{}, runtime.NumCPU()), |
||||
} |
||||
|
||||
return b, nil |
||||
} |
||||
|
||||
// Close ends the build and cleans up temporary files.
|
||||
func (b *Build) Close() error { |
||||
return os.RemoveAll(b.Tmp) |
||||
} |
||||
|
||||
// Build builds all targets concurrently.
|
||||
func (b *Build) Build(targets []Target) (files []string, err error) { |
||||
if len(targets) == 0 { |
||||
return nil, errors.New("no targets specified") |
||||
} |
||||
log.Printf("Building %d targets: %v", len(targets), targets) |
||||
var ( |
||||
wg sync.WaitGroup |
||||
errs = make([]error, len(targets)) |
||||
buildFiles = make([][]string, len(targets)) |
||||
) |
||||
for i, t := range targets { |
||||
wg.Add(1) |
||||
go func(i int, t Target) { |
||||
var err error |
||||
defer func() { |
||||
errs[i] = err |
||||
wg.Done() |
||||
}() |
||||
fs, err := t.Build(b) |
||||
buildFiles[i] = fs |
||||
}(i, t) |
||||
} |
||||
wg.Wait() |
||||
|
||||
for _, fs := range buildFiles { |
||||
files = append(files, fs...) |
||||
} |
||||
sort.Strings(files) |
||||
|
||||
return files, multierr.New(errs...) |
||||
} |
||||
|
||||
// Once runs fn if Once hasn't been called with name before.
|
||||
func (b *Build) Once(name string, fn func() error) error { |
||||
return b.once.Do(name, fn) |
||||
} |
||||
|
||||
// Extra returns a value from the build's extra state, creating it if necessary.
|
||||
func (b *Build) Extra(key any, constructor func() any) any { |
||||
b.extraMu.Lock() |
||||
defer b.extraMu.Unlock() |
||||
ret, ok := b.extra[key] |
||||
if !ok { |
||||
ret = constructor() |
||||
b.extra[key] = ret |
||||
} |
||||
return ret |
||||
} |
||||
|
||||
// GoPkg returns the path on disk of pkg.
|
||||
// The module of pkg must be imported in b.Repo's go.mod.
|
||||
func (b *Build) GoPkg(pkg string) (string, error) { |
||||
bs, err := exec.Command(b.Go, "list", "-f", "{{.Dir}}", pkg).Output() |
||||
if err != nil { |
||||
return "", fmt.Errorf("finding package %q: %w", pkg, err) |
||||
} |
||||
return strings.TrimSpace(string(bs)), nil |
||||
} |
||||
|
||||
// TmpDir creates and returns a new empty temporary directory.
|
||||
// The caller does not need to clean up the directory after use, it will get
|
||||
// deleted by b.Close().
|
||||
func (b *Build) TmpDir() string { |
||||
// Because we're creating all temp dirs in our parent temp dir, the only
|
||||
// failures that can happen at this point are sequence breaks (e.g. if b.Tmp
|
||||
// is deleted while stuff is still running). So, panic on error to slightly
|
||||
// simplify callsites.
|
||||
ret, err := os.MkdirTemp(b.Tmp, "") |
||||
if err != nil { |
||||
panic(fmt.Sprintf("creating temp dir: %v", err)) |
||||
} |
||||
return ret |
||||
} |
||||
|
||||
// BuildGoBinary builds the Go binary at path and returns the path to the
|
||||
// binary. Builds are cached by path and env, so each build only happens once
|
||||
// per process execution.
|
||||
func (b *Build) BuildGoBinary(path string, env map[string]string) (string, error) { |
||||
err := b.Once("init-go", func() error { |
||||
log.Printf("Initializing Go toolchain") |
||||
// If the build is using a tool/go, it may need to download a toolchain
|
||||
// and do other initialization. Running `go version` once takes care of
|
||||
// all of that and avoids that initialization happening concurrently
|
||||
// later on in builds.
|
||||
_, err := exec.Command(b.Go, "version").Output() |
||||
return err |
||||
}) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
buildKey := []any{"go-build", path, env} |
||||
return b.goBuilds.Do(buildKey, func() (string, error) { |
||||
b.goBuildLimit <- struct{}{} |
||||
defer func() { <-b.goBuildLimit }() |
||||
|
||||
var envStrs []string |
||||
for k, v := range env { |
||||
envStrs = append(envStrs, k+"="+v) |
||||
} |
||||
sort.Strings(envStrs) |
||||
log.Printf("Building %s (with env %s)", path, strings.Join(envStrs, " ")) |
||||
buildDir := b.TmpDir() |
||||
cmd := exec.Command(b.Go, "build", "-o", buildDir, path) |
||||
cmd.Dir = b.Repo |
||||
cmd.Env = os.Environ() |
||||
for k, v := range env { |
||||
cmd.Env = append(cmd.Env, k+"="+v) |
||||
} |
||||
cmd.Env = append(cmd.Env, "TS_USE_GOCROSS=1") |
||||
cmd.Stdout = os.Stdout |
||||
cmd.Stderr = os.Stderr |
||||
if err := cmd.Run(); err != nil { |
||||
return "", err |
||||
} |
||||
out := filepath.Join(buildDir, filepath.Base(path)) |
||||
if env["GOOS"] == "windows" || env["GOOS"] == "windowsgui" { |
||||
out += ".exe" |
||||
} |
||||
return out, nil |
||||
}) |
||||
} |
||||
|
||||
func findModRoot(path string) (string, error) { |
||||
for { |
||||
modpath := filepath.Join(path, "go.mod") |
||||
if _, err := os.Stat(modpath); err == nil { |
||||
return path, nil |
||||
} else if !errors.Is(err, os.ErrNotExist) { |
||||
return "", err |
||||
} |
||||
path = filepath.Dir(path) |
||||
if path == "/" { |
||||
return "", fmt.Errorf("no go.mod found in %q or any parent directory", path) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func findGo(path string) (string, error) { |
||||
toolGo := filepath.Join(path, "tool/go") |
||||
if _, err := os.Stat(toolGo); err == nil { |
||||
return toolGo, nil |
||||
} |
||||
toolGo, err := exec.LookPath("go") |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
return toolGo, nil |
||||
} |
||||
|
||||
// FilterTargets returns the subset of targets that match any of the filters.
|
||||
// If filters is empty, returns all targets.
|
||||
func FilterTargets(targets []Target, filters []string) ([]Target, error) { |
||||
var filts []*regexp.Regexp |
||||
for _, f := range filters { |
||||
if f == "all" { |
||||
return targets, nil |
||||
} |
||||
filt, err := regexp.Compile(f) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("invalid filter %q: %w", f, err) |
||||
} |
||||
filts = append(filts, filt) |
||||
} |
||||
var ret []Target |
||||
for _, t := range targets { |
||||
for _, filt := range filts { |
||||
if filt.MatchString(t.String()) { |
||||
ret = append(ret, t) |
||||
break |
||||
} |
||||
} |
||||
} |
||||
return ret, nil |
||||
} |
||||
@ -0,0 +1,83 @@ |
||||
package dist |
||||
|
||||
import ( |
||||
"sync" |
||||
|
||||
"tailscale.com/util/deephash" |
||||
) |
||||
|
||||
// MemoizedFn is a function that memoize.Do can call.
|
||||
type MemoizedFn[T any] func() (T, error) |
||||
|
||||
// Memoize runs MemoizedFns and remembers their results.
|
||||
type Memoize[O any] struct { |
||||
mu sync.Mutex |
||||
cond *sync.Cond |
||||
outs map[deephash.Sum]O |
||||
errs map[deephash.Sum]error |
||||
inflight map[deephash.Sum]bool |
||||
} |
||||
|
||||
// Do runs fn and returns its result.
|
||||
// fn is only run once per unique key. Subsequent Do calls with the same key
|
||||
// return the memoized result of the first call, even if fn is a different
|
||||
// function.
|
||||
func (m *Memoize[O]) Do(key any, fn MemoizedFn[O]) (ret O, err error) { |
||||
m.mu.Lock() |
||||
defer m.mu.Unlock() |
||||
if m.cond == nil { |
||||
m.cond = sync.NewCond(&m.mu) |
||||
m.outs = map[deephash.Sum]O{} |
||||
m.errs = map[deephash.Sum]error{} |
||||
m.inflight = map[deephash.Sum]bool{} |
||||
} |
||||
|
||||
k := deephash.Hash(&key) |
||||
|
||||
for m.inflight[k] { |
||||
m.cond.Wait() |
||||
} |
||||
if err := m.errs[k]; err != nil { |
||||
var ret O |
||||
return ret, err |
||||
} |
||||
if ret, ok := m.outs[k]; ok { |
||||
return ret, nil |
||||
} |
||||
|
||||
m.inflight[k] = true |
||||
m.mu.Unlock() |
||||
defer func() { |
||||
m.mu.Lock() |
||||
delete(m.inflight, k) |
||||
if err != nil { |
||||
m.errs[k] = err |
||||
} else { |
||||
m.outs[k] = ret |
||||
} |
||||
m.cond.Broadcast() |
||||
}() |
||||
|
||||
ret, err = fn() |
||||
if err != nil { |
||||
var ret O |
||||
return ret, err |
||||
} |
||||
return ret, nil |
||||
} |
||||
|
||||
// once is like memoize, but for functions that don't return non-error values.
|
||||
type once struct { |
||||
m Memoize[any] |
||||
} |
||||
|
||||
// Do runs fn.
|
||||
// fn is only run once per unique key. Subsequent Do calls with the same key
|
||||
// return the memoized result of the first call, even if fn is a different
|
||||
// function.
|
||||
func (o *once) Do(key any, fn func() error) error { |
||||
_, err := o.m.Do(key, func() (any, error) { |
||||
return nil, fn() |
||||
}) |
||||
return err |
||||
} |
||||
@ -0,0 +1,375 @@ |
||||
// Package unixpkgs contains dist Targets for building unix Tailscale packages.
|
||||
package unixpkgs |
||||
|
||||
import ( |
||||
"archive/tar" |
||||
"compress/gzip" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"log" |
||||
"os" |
||||
"path/filepath" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/goreleaser/nfpm" |
||||
"tailscale.com/release/dist" |
||||
) |
||||
|
||||
type tgzTarget struct { |
||||
filenameArch string // arch to use in filename instead of deriving from goenv["GOARCH"]
|
||||
goenv map[string]string |
||||
} |
||||
|
||||
func (t *tgzTarget) arch() string { |
||||
if t.filenameArch != "" { |
||||
return t.filenameArch |
||||
} |
||||
return t.goenv["GOARCH"] |
||||
} |
||||
|
||||
func (t *tgzTarget) os() string { |
||||
return t.goenv["GOOS"] |
||||
} |
||||
|
||||
func (t *tgzTarget) String() string { |
||||
return fmt.Sprintf("%s/%s/tgz", t.os(), t.arch()) |
||||
} |
||||
|
||||
func (t *tgzTarget) Build(b *dist.Build) ([]string, error) { |
||||
var filename string |
||||
if t.goenv["GOOS"] == "linux" { |
||||
// Linux used to be the only tgz architecture, so we didn't put the OS
|
||||
// name in the filename.
|
||||
filename = fmt.Sprintf("tailscale_%s_%s.tgz", b.Version.Short, t.arch()) |
||||
} else { |
||||
filename = fmt.Sprintf("tailscale_%s_%s_%s.tgz", b.Version.Short, t.os(), t.arch()) |
||||
} |
||||
ts, err := b.BuildGoBinary("tailscale.com/cmd/tailscale", t.goenv) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
tsd, err := b.BuildGoBinary("tailscale.com/cmd/tailscaled", t.goenv) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
log.Printf("Building %s", filename) |
||||
|
||||
out := filepath.Join(b.Out, filename) |
||||
f, err := os.Create(out) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer f.Close() |
||||
gw := gzip.NewWriter(f) |
||||
defer gw.Close() |
||||
tw := tar.NewWriter(gw) |
||||
defer tw.Close() |
||||
|
||||
buildTime := time.Now() |
||||
addFile := func(src, dst string, mode int64) error { |
||||
f, err := os.Open(src) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer f.Close() |
||||
fi, err := f.Stat() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
hdr := &tar.Header{ |
||||
Name: dst, |
||||
Size: fi.Size(), |
||||
Mode: mode, |
||||
ModTime: buildTime, |
||||
Uid: 0, |
||||
Gid: 0, |
||||
Uname: "root", |
||||
Gname: "root", |
||||
} |
||||
if err := tw.WriteHeader(hdr); err != nil { |
||||
return err |
||||
} |
||||
if _, err = io.Copy(tw, f); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
addDir := func(name string) error { |
||||
hdr := &tar.Header{ |
||||
Name: name + "/", |
||||
Mode: 0755, |
||||
ModTime: buildTime, |
||||
Uid: 0, |
||||
Gid: 0, |
||||
Uname: "root", |
||||
Gname: "root", |
||||
} |
||||
return tw.WriteHeader(hdr) |
||||
} |
||||
dir := strings.TrimSuffix(filename, ".tgz") |
||||
if err := addDir(dir); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := addFile(tsd, filepath.Join(dir, "tailscaled"), 0755); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := addFile(ts, filepath.Join(dir, "tailscale"), 0755); err != nil { |
||||
return nil, err |
||||
} |
||||
if t.os() == "linux" { |
||||
dir = filepath.Join(dir, "systemd") |
||||
if err := addDir(dir); err != nil { |
||||
return nil, err |
||||
} |
||||
tailscaledDir, err := b.GoPkg("tailscale.com/cmd/tailscaled") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if err := addFile(filepath.Join(tailscaledDir, "tailscaled.service"), filepath.Join(dir, "tailscaled.service"), 0644); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := addFile(filepath.Join(tailscaledDir, "tailscaled.defaults"), filepath.Join(dir, "tailscaled.defaults"), 0644); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
if err := tw.Close(); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := gw.Close(); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := f.Close(); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return []string{filename}, nil |
||||
} |
||||
|
||||
type debTarget struct { |
||||
goenv map[string]string |
||||
} |
||||
|
||||
func (t *debTarget) os() string { |
||||
return t.goenv["GOOS"] |
||||
} |
||||
|
||||
func (t *debTarget) arch() string { |
||||
return t.goenv["GOARCH"] |
||||
} |
||||
|
||||
func (t *debTarget) String() string { |
||||
return fmt.Sprintf("linux/%s/deb", t.goenv["GOARCH"]) |
||||
} |
||||
|
||||
func (t *debTarget) Build(b *dist.Build) ([]string, error) { |
||||
if t.os() != "linux" { |
||||
return nil, errors.New("deb only supported on linux") |
||||
} |
||||
|
||||
ts, err := b.BuildGoBinary("tailscale.com/cmd/tailscale", t.goenv) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
tsd, err := b.BuildGoBinary("tailscale.com/cmd/tailscaled", t.goenv) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
tailscaledDir, err := b.GoPkg("tailscale.com/cmd/tailscaled") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
repoDir, err := b.GoPkg("tailscale.com") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
arch := debArch(t.arch()) |
||||
info := nfpm.WithDefaults(&nfpm.Info{ |
||||
Name: "tailscale", |
||||
Arch: arch, |
||||
Platform: "linux", |
||||
Version: b.Version.Short, |
||||
Maintainer: "Tailscale Inc <info@tailscale.com>", |
||||
Description: "The easiest, most secure, cross platform way to use WireGuard + oauth2 + 2FA/SSO", |
||||
Homepage: "https://www.tailscale.com", |
||||
License: "MIT", |
||||
Section: "net", |
||||
Priority: "extra", |
||||
Overridables: nfpm.Overridables{ |
||||
Files: map[string]string{ |
||||
ts: "/usr/bin/tailscale", |
||||
tsd: "/usr/sbin/tailscaled", |
||||
filepath.Join(tailscaledDir, "tailscaled.service"): "/lib/systemd/system/tailscaled.service", |
||||
}, |
||||
ConfigFiles: map[string]string{ |
||||
filepath.Join(tailscaledDir, "tailscaled.defaults"): "/etc/default/tailscaled", |
||||
}, |
||||
Scripts: nfpm.Scripts{ |
||||
PostInstall: filepath.Join(repoDir, "release/deb/debian.postinst.sh"), |
||||
PreRemove: filepath.Join(repoDir, "release/deb/debian.prerm.sh"), |
||||
PostRemove: filepath.Join(repoDir, "release/deb/debian.postrm.sh"), |
||||
}, |
||||
Depends: []string{"iptables", "iproute2"}, |
||||
Recommends: []string{"tailscale-archive-keyring (>= 1.35.181)"}, |
||||
Replaces: []string{"tailscale-relay"}, |
||||
Conflicts: []string{"tailscale-relay"}, |
||||
}, |
||||
}) |
||||
pkg, err := nfpm.Get("deb") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
filename := fmt.Sprintf("tailscale_%s_%s.deb", b.Version.Short, arch) |
||||
log.Printf("Building %s", filename) |
||||
f, err := os.Create(filepath.Join(b.Out, filename)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer f.Close() |
||||
if err := pkg.Package(info, f); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := f.Close(); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return []string{filename}, nil |
||||
} |
||||
|
||||
type rpmTarget struct { |
||||
goenv map[string]string |
||||
} |
||||
|
||||
func (t *rpmTarget) os() string { |
||||
return t.goenv["GOOS"] |
||||
} |
||||
|
||||
func (t *rpmTarget) arch() string { |
||||
return t.goenv["GOARCH"] |
||||
} |
||||
|
||||
func (t *rpmTarget) String() string { |
||||
return fmt.Sprintf("linux/%s/rpm", t.arch()) |
||||
} |
||||
|
||||
func (t *rpmTarget) Build(b *dist.Build) ([]string, error) { |
||||
if t.os() != "linux" { |
||||
return nil, errors.New("rpm only supported on linux") |
||||
} |
||||
|
||||
ts, err := b.BuildGoBinary("tailscale.com/cmd/tailscale", t.goenv) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
tsd, err := b.BuildGoBinary("tailscale.com/cmd/tailscaled", t.goenv) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
tailscaledDir, err := b.GoPkg("tailscale.com/cmd/tailscaled") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
repoDir, err := b.GoPkg("tailscale.com") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
arch := rpmArch(t.arch()) |
||||
info := nfpm.WithDefaults(&nfpm.Info{ |
||||
Name: "tailscale", |
||||
Arch: arch, |
||||
Platform: "linux", |
||||
Version: b.Version.Short, |
||||
Maintainer: "Tailscale Inc <info@tailscale.com>", |
||||
Description: "The easiest, most secure, cross platform way to use WireGuard + oauth2 + 2FA/SSO", |
||||
Homepage: "https://www.tailscale.com", |
||||
License: "MIT", |
||||
Overridables: nfpm.Overridables{ |
||||
Files: map[string]string{ |
||||
ts: "/usr/bin/tailscale", |
||||
tsd: "/usr/sbin/tailscaled", |
||||
filepath.Join(tailscaledDir, "tailscaled.service"): "/lib/systemd/system/tailscaled.service", |
||||
}, |
||||
ConfigFiles: map[string]string{ |
||||
filepath.Join(tailscaledDir, "tailscaled.defaults"): "/etc/default/tailscaled", |
||||
}, |
||||
// SELinux policy on e.g. CentOS 8 forbids writing to /var/cache.
|
||||
// Creating an empty directory at install time resolves this issue.
|
||||
EmptyFolders: []string{"/var/cache/tailscale"}, |
||||
Scripts: nfpm.Scripts{ |
||||
PostInstall: filepath.Join(repoDir, "release/rpm/rpm.postinst.sh"), |
||||
PreRemove: filepath.Join(repoDir, "release/rpm/rpm.prerm.sh"), |
||||
PostRemove: filepath.Join(repoDir, "release/rpm/rpm.postrm.sh"), |
||||
}, |
||||
Depends: []string{"iptables", "iproute"}, |
||||
Replaces: []string{"tailscale-relay"}, |
||||
Conflicts: []string{"tailscale-relay"}, |
||||
RPM: nfpm.RPM{ |
||||
Group: "Network", |
||||
}, |
||||
}, |
||||
}) |
||||
pkg, err := nfpm.Get("rpm") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
filename := fmt.Sprintf("tailscale_%s_%s.rpm", b.Version.Short, arch) |
||||
log.Printf("Building %s", filename) |
||||
|
||||
f, err := os.Create(filepath.Join(b.Out, filename)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer f.Close() |
||||
if err := pkg.Package(info, f); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := f.Close(); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return []string{filename}, nil |
||||
} |
||||
|
||||
// debArch returns the debian arch name for the given Go arch name.
|
||||
// nfpm also does this translation internally, but we need to do it outside nfpm
|
||||
// because we also need the filename to be correct.
|
||||
func debArch(arch string) string { |
||||
switch arch { |
||||
case "386": |
||||
return "i386" |
||||
case "arm": |
||||
// TODO: this is supposed to be "armel" for GOARM=5, and "armhf" for
|
||||
// GOARM=6 and 7. But we have some tech debt to pay off here before we
|
||||
// can ship more than 1 ARM deb, so for now match redo's behavior of
|
||||
// shipping armv5 binaries in an armv7 trenchcoat.
|
||||
return "armhf" |
||||
default: |
||||
return arch |
||||
} |
||||
} |
||||
|
||||
// rpmArch returns the RPM arch name for the given Go arch name.
|
||||
// nfpm also does this translation internally, but we need to do it outside nfpm
|
||||
// because we also need the filename to be correct.
|
||||
func rpmArch(arch string) string { |
||||
switch arch { |
||||
case "amd64": |
||||
return "x86_64" |
||||
case "386": |
||||
return "i386" |
||||
case "arm": |
||||
return "armv7hl" |
||||
case "arm64": |
||||
return "aarch64" |
||||
default: |
||||
return arch |
||||
} |
||||
} |
||||
@ -0,0 +1,116 @@ |
||||
package unixpkgs |
||||
|
||||
import ( |
||||
"fmt" |
||||
"sort" |
||||
"strings" |
||||
|
||||
"tailscale.com/release/dist" |
||||
|
||||
_ "github.com/goreleaser/nfpm/deb" |
||||
_ "github.com/goreleaser/nfpm/rpm" |
||||
) |
||||
|
||||
func Targets() []dist.Target { |
||||
var ret []dist.Target |
||||
for goosgoarch := range tarballs { |
||||
goos, goarch := splitGoosGoarch(goosgoarch) |
||||
ret = append(ret, &tgzTarget{ |
||||
goenv: map[string]string{ |
||||
"GOOS": goos, |
||||
"GOARCH": goarch, |
||||
}, |
||||
}) |
||||
} |
||||
for goosgoarch := range debs { |
||||
goos, goarch := splitGoosGoarch(goosgoarch) |
||||
ret = append(ret, &debTarget{ |
||||
goenv: map[string]string{ |
||||
"GOOS": goos, |
||||
"GOARCH": goarch, |
||||
}, |
||||
}) |
||||
} |
||||
for goosgoarch := range rpms { |
||||
goos, goarch := splitGoosGoarch(goosgoarch) |
||||
ret = append(ret, &rpmTarget{ |
||||
goenv: map[string]string{ |
||||
"GOOS": goos, |
||||
"GOARCH": goarch, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
// Special case: AMD Geode is 386 with softfloat. Tarballs only since it's
|
||||
// an ancient architecture.
|
||||
ret = append(ret, &tgzTarget{ |
||||
filenameArch: "geode", |
||||
goenv: map[string]string{ |
||||
"GOOS": "linux", |
||||
"GOARCH": "386", |
||||
"GO386": "softfloat", |
||||
}, |
||||
}) |
||||
|
||||
sort.Slice(ret, func(i, j int) bool { |
||||
return ret[i].String() < ret[j].String() |
||||
}) |
||||
|
||||
return ret |
||||
} |
||||
|
||||
var ( |
||||
tarballs = map[string]bool{ |
||||
"linux/386": true, |
||||
"linux/amd64": true, |
||||
"linux/arm": true, |
||||
"linux/arm64": true, |
||||
"linux/mips64": true, |
||||
"linux/mips64le": true, |
||||
"linux/mips": true, |
||||
"linux/mipsle": true, |
||||
"linux/riscv64": true, |
||||
// TODO: more tarballs we could distribute, but don't currently. Leaving
|
||||
// out for initial parity with redo.
|
||||
// "darwin/amd64": true,
|
||||
// "darwin/arm64": true,
|
||||
// "freebsd/amd64": true,
|
||||
// "openbsd/amd64": true,
|
||||
} |
||||
|
||||
debs = map[string]bool{ |
||||
"linux/386": true, |
||||
"linux/amd64": true, |
||||
"linux/arm": true, |
||||
"linux/arm64": true, |
||||
"linux/riscv64": true, |
||||
// TODO: maybe mipses, we accidentally started building them at some
|
||||
// point even though they probably don't work right.
|
||||
// "linux/mips": true,
|
||||
// "linux/mipsle": true,
|
||||
// "linux/mips64": true,
|
||||
// "linux/mips64le": true,
|
||||
} |
||||
|
||||
rpms = map[string]bool{ |
||||
"linux/386": true, |
||||
"linux/amd64": true, |
||||
"linux/arm": true, |
||||
"linux/arm64": true, |
||||
"linux/riscv64": true, |
||||
// TODO: maybe mipses, we accidentally started building them at some
|
||||
// point even though they probably don't work right.
|
||||
// "linux/mips": true,
|
||||
// "linux/mipsle": true,
|
||||
// "linux/mips64": true,
|
||||
// "linux/mips64le": true,
|
||||
} |
||||
) |
||||
|
||||
func splitGoosGoarch(s string) (string, string) { |
||||
goos, goarch, ok := strings.Cut(s, "/") |
||||
if !ok { |
||||
panic(fmt.Sprintf("invalid target %q", s)) |
||||
} |
||||
return goos, goarch |
||||
} |
||||
@ -0,0 +1,41 @@ |
||||
# $1 == 1 for initial installation. |
||||
# $1 == 2 for upgrades. |
||||
|
||||
if [ $1 -eq 1 ] ; then |
||||
# Normally, the tailscale-relay package would request shutdown of |
||||
# its service before uninstallation. Unfortunately, the |
||||
# tailscale-relay package we distributed doesn't have those |
||||
# scriptlets. We definitely want relaynode to be stopped when |
||||
# installing tailscaled though, so we blindly try to turn off |
||||
# relaynode here. |
||||
# |
||||
# However, we also want this package installation to look like an |
||||
# upgrade from relaynode! Therefore, if relaynode is currently |
||||
# enabled, we want to also enable tailscaled. If relaynode is |
||||
# currently running, we also want to start tailscaled. |
||||
# |
||||
# If there doesn't seem to be an active or enabled relaynode on |
||||
# the system, we follow the RPM convention for package installs, |
||||
# which is to not enable or start the service. |
||||
relaynode_enabled=0 |
||||
relaynode_running=0 |
||||
if systemctl is-enabled tailscale-relay.service >/dev/null 2>&1; then |
||||
relaynode_enabled=1 |
||||
fi |
||||
if systemctl is-active tailscale-relay.service >/dev/null 2>&1; then |
||||
relaynode_running=1 |
||||
fi |
||||
|
||||
systemctl --no-reload disable tailscale-relay.service >/dev/null 2>&1 || : |
||||
systemctl stop tailscale-relay.service >/dev/null 2>&1 || : |
||||
|
||||
if [ $relaynode_enabled -eq 1 ]; then |
||||
systemctl enable tailscaled.service >/dev/null 2>&1 || : |
||||
else |
||||
systemctl preset tailscaled.service >/dev/null 2>&1 || : |
||||
fi |
||||
|
||||
if [ $relaynode_running -eq 1 ]; then |
||||
systemctl start tailscaled.service >/dev/null 2>&1 || : |
||||
fi |
||||
fi |
||||
@ -0,0 +1,8 @@ |
||||
# $1 == 0 for uninstallation. |
||||
# $1 == 1 for removing old package during upgrade. |
||||
|
||||
systemctl daemon-reload >/dev/null 2>&1 || : |
||||
if [ $1 -ge 1 ] ; then |
||||
# Package upgrade, not uninstall |
||||
systemctl try-restart tailscaled.service >/dev/null 2>&1 || : |
||||
fi |
||||
@ -0,0 +1,8 @@ |
||||
# $1 == 0 for uninstallation. |
||||
# $1 == 1 for removing old package during upgrade. |
||||
|
||||
if [ $1 -eq 0 ] ; then |
||||
# Package removal, not upgrade |
||||
systemctl --no-reload disable tailscaled.service > /dev/null 2>&1 || : |
||||
systemctl stop tailscaled.service > /dev/null 2>&1 || : |
||||
fi |
||||
Loading…
Reference in new issue