release/dist/cli: factor out the CLI boilerplace from cmd/dist

Signed-off-by: David Anderson <danderson@tailscale.com>
pull/7370/head
David Anderson 2 years ago committed by Dave Anderson
parent fc4b25d9fd
commit cf74ee49ee

123
cmd/dist/dist.go vendored

@ -5,130 +5,21 @@ import (
"context" "context"
"errors" "errors"
"flag" "flag"
"fmt"
"log" "log"
"os" "os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/release/dist" "tailscale.com/release/dist"
"tailscale.com/release/dist/cli"
"tailscale.com/release/dist/unixpkgs" "tailscale.com/release/dist/unixpkgs"
) )
func main() { func getTargets() ([]dist.Target, error) {
var targets []dist.Target return unixpkgs.Targets(), nil
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 { func main() {
tgts, err := dist.FilterTargets(targets, filters) cmd := cli.CLI(getTargets)
if err != nil { if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil && !errors.Is(err, flag.ErrHelp) {
return err log.Fatal(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,134 @@
// Package cli provides the skeleton of a CLI for building release packages.
package cli
import (
"context"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/release/dist"
)
// CLI returns a CLI root command to build release packages.
//
// getTargets is a function that gets run in the Exec function of commands that
// need to know the target list. Its execution is deferred in this way to allow
// customization of command FlagSets with flags that influence the target list.
func CLI(getTargets func() ([]dist.Target, error)) *ffcli.Command {
return &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 {
targets, err := getTargets()
if err != nil {
return err
}
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 {
targets, err := getTargets()
if err != nil {
return err
}
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 },
}
}
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
}
Loading…
Cancel
Save