diff --git a/release/dist/cli/cli.go b/release/dist/cli/cli.go index 0067b54a0..8445613e1 100644 --- a/release/dist/cli/cli.go +++ b/release/dist/cli/cli.go @@ -70,6 +70,7 @@ func CLI(getTargets func(unixpkgs.Signers) ([]dist.Target, error)) *ffcli.Comman fs.StringVar(&buildArgs.manifest, "manifest", "", "manifest file to write") fs.BoolVar(&buildArgs.verbose, "verbose", false, "verbose logging") fs.StringVar(&buildArgs.tgzSigningKey, "tgz-signing-key", "", "path to private signing key for release tarballs") + fs.StringVar(&buildArgs.webClientRoot, "web-client-root", "", "path to root of web client source to build") return fs })(), LongHelp: strings.TrimSpace(` @@ -100,6 +101,7 @@ var buildArgs struct { manifest string verbose bool tgzSigningKey string + webClientRoot string } func runBuild(ctx context.Context, filters []string, targets []dist.Target) error { @@ -122,6 +124,7 @@ func runBuild(ctx context.Context, filters []string, targets []dist.Target) erro } defer b.Close() b.Verbose = buildArgs.verbose + b.WebClientSource = buildArgs.webClientRoot out, err := b.Build(tgts) if err != nil { diff --git a/release/dist/dist.go b/release/dist/dist.go index 8d9edd8e2..41ffe05f7 100644 --- a/release/dist/dist.go +++ b/release/dist/dist.go @@ -38,11 +38,16 @@ type Build struct { // Verbose is whether to print all command output, rather than just failed // commands. Verbose bool + // WebClientSource is a path to the source for the web client. + // If non-empty, web client assets will be built. + WebClientSource string // Tmp is a temporary directory that gets deleted when the Builder is closed. Tmp string // Go is the path to the Go binary to use for building. Go string + // Yarn is the path to the yarn binary to use for building the web client assets. + Yarn string // Version is the version info of the build. Version mkversion.VersionInfo // Time is the timestamp of the build. @@ -79,15 +84,20 @@ func NewBuild(repo, out string) (*Build, error) { if err != nil { return nil, fmt.Errorf("finding module root: %w", err) } - goTool, err := findGo(repo) + goTool, err := findTool(repo, "go") if err != nil { return nil, fmt.Errorf("finding go binary: %w", err) } + yarnTool, err := findTool(repo, "yarn") + if err != nil { + return nil, fmt.Errorf("finding yarn binary: %w", err) + } b := &Build{ Repo: repo, Tmp: tmp, Out: out, Go: goTool, + Yarn: yarnTool, Version: mkversion.Info(), Time: time.Now().UTC(), extra: map[any]any{}, @@ -180,6 +190,27 @@ func (b *Build) TmpDir() string { return ret } +// BuildWebClientAssets builds the JS and CSS assets used by the web client. +// If b.WebClientSource is non-empty, assets are built in a "build" sub-directory of that path. +// Otherwise, no assets are built. +func (b *Build) BuildWebClientAssets() error { + // Nothing in the web client assets is platform-specific, + // so we only need to build it once. + return b.Once("build-web-client-assets", func() error { + if b.WebClientSource == "" { + return nil + } + dir := b.WebClientSource + if err := b.Command(dir, b.Yarn, "install").Run(); err != nil { + return err + } + if err := b.Command(dir, b.Yarn, "build").Run(); err != nil { + return err + } + return nil + }) +} + // 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. @@ -303,16 +334,19 @@ func findModRoot(path string) (string, error) { } } -func findGo(path string) (string, error) { - toolGo := filepath.Join(path, "tool/go") - if _, err := os.Stat(toolGo); err == nil { - return toolGo, nil +// findTool returns the path to the specified named tool. +// It first looks in the "tool" directory in the provided path, +// then in the $PATH environment variable. +func findTool(path, name string) (string, error) { + tool := filepath.Join(path, "tool", name) + if _, err := os.Stat(tool); err == nil { + return tool, nil } - toolGo, err := exec.LookPath("go") + tool, err := exec.LookPath(name) if err != nil { return "", err } - return toolGo, nil + return tool, nil } // FilterTargets returns the subset of targets that match any of the filters. diff --git a/release/dist/synology/pkgs.go b/release/dist/synology/pkgs.go index 00660dbba..67deab4d1 100644 --- a/release/dist/synology/pkgs.go +++ b/release/dist/synology/pkgs.go @@ -144,6 +144,9 @@ func getSynologyBuilds(b *dist.Build) *synologyBuilds { func (m *synologyBuilds) buildInnerPackage(b *dist.Build, dsmVersion int, goenv map[string]string) (*innerPkg, error) { key := []any{dsmVersion, goenv} return m.innerPkgs.Do(key, func() (*innerPkg, error) { + if err := b.BuildWebClientAssets(); err != nil { + return nil, err + } ts, err := b.BuildGoBinary("tailscale.com/cmd/tailscale", goenv) if err != nil { return nil, err diff --git a/release/dist/unixpkgs/pkgs.go b/release/dist/unixpkgs/pkgs.go index 0b45f5c11..9047cd096 100644 --- a/release/dist/unixpkgs/pkgs.go +++ b/release/dist/unixpkgs/pkgs.go @@ -53,6 +53,9 @@ func (t *tgzTarget) Build(b *dist.Build) ([]string, error) { } else { filename = fmt.Sprintf("tailscale_%s_%s_%s.tgz", b.Version.Short, t.os(), t.arch()) } + if err := b.BuildWebClientAssets(); err != nil { + return nil, err + } ts, err := b.BuildGoBinary("tailscale.com/cmd/tailscale", t.goEnv) if err != nil { return nil, err @@ -193,6 +196,9 @@ func (t *debTarget) Build(b *dist.Build) ([]string, error) { return nil, errors.New("deb only supported on linux") } + if err := b.BuildWebClientAssets(); err != nil { + return nil, err + } ts, err := b.BuildGoBinary("tailscale.com/cmd/tailscale", t.goEnv) if err != nil { return nil, err @@ -305,6 +311,9 @@ func (t *rpmTarget) Build(b *dist.Build) ([]string, error) { return nil, errors.New("rpm only supported on linux") } + if err := b.BuildWebClientAssets(); err != nil { + return nil, err + } ts, err := b.BuildGoBinary("tailscale.com/cmd/tailscale", t.goEnv) if err != nil { return nil, err diff --git a/tool/yarn b/tool/yarn index 48c61c564..6357beda6 100755 --- a/tool/yarn +++ b/tool/yarn @@ -21,7 +21,7 @@ fi cachedir="$HOME/.cache/tailscale-yarn" tarball="${cachedir}.tar.gz" - read -r want_rev < "$(dirname "$0")/yarn.rev" + read -r want_rev < "./tool/yarn.rev" got_rev="" if [[ -x "${cachedir}/bin/yarn" ]]; then