Skip to content

Commit

Permalink
feat: allow language plugins to expose flags for module creation
Browse files Browse the repository at this point in the history
  • Loading branch information
matt2e committed Oct 1, 2024
1 parent 44bb5e5 commit bbb404b
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 16 deletions.
70 changes: 64 additions & 6 deletions frontend/cli/cmd_new.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,77 @@ import (
"go/token"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"

"github.com/TBD54566975/ftl/backend/schema"
"github.com/TBD54566975/ftl/internal"
"github.com/TBD54566975/ftl/internal/buildengine"
"github.com/TBD54566975/ftl/internal/log"
"github.com/TBD54566975/ftl/internal/moduleconfig"
"github.com/TBD54566975/ftl/internal/projectconfig"
"github.com/TBD54566975/ftl/internal/slices"
"github.com/alecthomas/kong"
)

type newCmd struct {
Language string `arg:"" help:"Language of the module to create."`
Dir string `arg:"" help:"Directory to initialize the module in."`
Name string `arg:"" help:"Name of the FTL module to create underneath the base directory."`
}

// prepareNewCmd adds language specific flags to kong
// This allows the new command to have good support for language specific flags like:
// - help text (ftl new go --help)
// - default values
// - environment variable overrides
func prepareNewCmd(ctx context.Context, k *kong.Kong, args []string) error {
if len(args) < 2 {
return nil
} else if args[0] != "new" {
return nil
}
language := args[1]
if len(language) == 0 {
return nil
}

newCmdNode, ok := slices.Find(k.Model.Children, func(n *kong.Node) bool {
return n.Name == "new"
})
if !ok {
return fmt.Errorf("could not find new command")
}

plugin, err := buildengine.PluginFromConfig(ctx, moduleconfig.ModuleConfig{
Language: language,
}, "")
if err != nil {
return err
}

// Go specific flags
Replace map[string]string `short:"r" help:"For Go, replace a module import path with a local path in the initialised FTL module." placeholder:"OLD=NEW,..." env:"FTL_INIT_GO_REPLACE"`
flags, err := plugin.GetCreateModuleFlags(ctx)
if err != nil {
return err
}

// Java/Kotlin specific flags
Group string `help:"For Java and Kotlin, the Maven groupId of the project." default:"com.example"`
registry := kong.NewRegistry().RegisterDefaults()
for _, flag := range flags {
var str string
strPtr := &str
flag.Target = reflect.ValueOf(strPtr).Elem()
flag.Mapper = registry.ForValue(flag.Target)
flag.Group = &kong.Group{
Title: "Flags for " + strings.ToTitle(language[0:1]) + language[1:] + " modules",
Key: "languageSpecificFlags",
}
}
newCmdNode.Flags = append(newCmdNode.Flags, flags...)
return nil
}

func (i newCmd) Run(ctx context.Context, config projectconfig.Config) error {
func (i newCmd) Run(ctx context.Context, ktctx *kong.Context, config projectconfig.Config) error {
name, path, err := validateModule(i.Dir, i.Name)
if err != nil {
return err
Expand All @@ -47,11 +95,21 @@ func (i newCmd) Run(ctx context.Context, config projectconfig.Config) error {
Language: i.Language,
Dir: path,
}

flags := map[string]string{}
for _, f := range ktctx.Selected().Flags {
flagValue, ok := f.Target.Interface().(string)
if !ok {
return fmt.Errorf("expected %v value to be a string but it was %T", f.Name, f.Target.Interface())
}
flags[f.Name] = flagValue
}

plugin, err := buildengine.PluginFromConfig(ctx, moduleConfig, config.Root())
if err != nil {
return err
}
err = plugin.CreateModule(ctx, moduleConfig, config.Hermit, i.Replace, i.Group)
err = plugin.CreateModule(ctx, config, moduleConfig, flags)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type InteractiveCLI struct {
Status statusCmd `cmd:"" help:"Show FTL status."`
Init initCmd `cmd:"" help:"Initialize a new FTL project."`
Profile profileCmd `cmd:"" help:"Manage profiles."`
New newCmd `cmd:"" help:"Create a new FTL module."`
New newCmd `cmd:"" help:"Create a new FTL module. See language specific flags with 'ftl new <language> --help'."`
PS psCmd `cmd:"" help:"List deployments."`
Call callCmd `cmd:"" help:"Call an FTL function."`
Bench benchCmd `cmd:"" help:"Benchmark an FTL function."`
Expand Down Expand Up @@ -79,8 +79,8 @@ var cli CLI

func main() {
ctx, cancel := context.WithCancel(context.Background())

app := createKongApplication(&cli)
prepareNewCmd(ctx, app, os.Args[1:])
kctx, err := app.Parse(os.Args[1:])
app.FatalIfErrorf(err)

Expand Down
8 changes: 6 additions & 2 deletions internal/buildengine/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"path/filepath"
"time"

"github.com/alecthomas/kong"
"github.com/alecthomas/types/either"
"github.com/alecthomas/types/pubsub"
"google.golang.org/protobuf/proto"
Expand All @@ -16,6 +17,7 @@ import (
"github.com/TBD54566975/ftl/internal/errors"
"github.com/TBD54566975/ftl/internal/flock"
"github.com/TBD54566975/ftl/internal/moduleconfig"
"github.com/TBD54566975/ftl/internal/projectconfig"
)

const BuildLockTimeout = time.Minute
Expand Down Expand Up @@ -58,9 +60,11 @@ type LanguagePlugin interface {
// The same topic must be returned each time this method is called
Updates() *pubsub.Topic[PluginEvent]

// GetCreateModuleFlags returns the flags that can be used to create a module for this language.
GetCreateModuleFlags(ctx context.Context) ([]*kong.Flag, error)

// CreateModule creates a new module in the given directory with the given name and language.
// Replacements and groups are special cases until plugins can provide their parameters.
CreateModule(ctx context.Context, config moduleconfig.ModuleConfig, includeBinDir bool, replacements map[string]string, group string) error
CreateModule(ctx context.Context, projConfig projectconfig.Config, moduleConfig moduleconfig.ModuleConfig, flags map[string]string) error

// GetDependencies returns the dependencies of the module.
GetDependencies(ctx context.Context) ([]string, error)
Expand Down
35 changes: 32 additions & 3 deletions internal/buildengine/plugin_go.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"

"github.com/TBD54566975/scaffolder"
"github.com/alecthomas/kong"
"golang.org/x/exp/maps"

"github.com/TBD54566975/ftl/backend/schema"
Expand All @@ -22,6 +23,7 @@ import (
"github.com/TBD54566975/ftl/internal/exec"
"github.com/TBD54566975/ftl/internal/log"
"github.com/TBD54566975/ftl/internal/moduleconfig"
"github.com/TBD54566975/ftl/internal/projectconfig"
)

type goPlugin struct {
Expand All @@ -43,20 +45,47 @@ type scaffoldingContext struct {
Replace map[string]string
}

func (p *goPlugin) CreateModule(ctx context.Context, c moduleconfig.ModuleConfig, includeBinDir bool, replacements map[string]string, group string) error {
func (p *goPlugin) GetCreateModuleFlags(ctx context.Context) ([]*kong.Flag, error) {
return []*kong.Flag{
{
Value: &kong.Value{
Name: "replace",
Help: "Replace a module import path with a local path in the initialised FTL module.",
Tag: &kong.Tag{
Envs: []string{"FTL_INIT_GO_REPLACE"},
},
},
Short: 'r',
PlaceHolder: "OLD=NEW,...",
},
}, nil
}

func (p *goPlugin) CreateModule(ctx context.Context, projConfig projectconfig.Config, c moduleconfig.ModuleConfig, flags map[string]string) error {
logger := log.FromContext(ctx)
config := c.Abs()

opts := []scaffolder.Option{
scaffolder.Exclude("^go.mod$"),
}
if !includeBinDir {
if !projConfig.Hermit {
logger.Debugf("Excluding bin directory")
opts = append(opts, scaffolder.Exclude("^bin"))
}

sctx := scaffoldingContext{
Name: config.Module,
GoVersion: runtime.Version()[2:],
Replace: replacements,
Replace: map[string]string{},
}
if replaceStr, ok := flags["replace"]; ok {
for _, replace := range strings.Split(replaceStr, ",") {
parts := strings.Split(replace, "=")
if len(parts) != 2 {
return fmt.Errorf("invalid replace flag (format: A=B,C=D): %q", replace)
}
sctx.Replace[parts[0]] = parts[1]
}
}

// scaffold at one directory above the module directory
Expand Down
25 changes: 23 additions & 2 deletions internal/buildengine/plugin_java.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"

"github.com/TBD54566975/scaffolder"
"github.com/alecthomas/kong"
"github.com/beevik/etree"
"golang.org/x/exp/maps"

Expand All @@ -22,6 +23,7 @@ import (
"github.com/TBD54566975/ftl/internal/exec"
"github.com/TBD54566975/ftl/internal/log"
"github.com/TBD54566975/ftl/internal/moduleconfig"
"github.com/TBD54566975/ftl/internal/projectconfig"
"github.com/TBD54566975/ftl/jvm-runtime/java"
"github.com/TBD54566975/ftl/jvm-runtime/kotlin"
)
Expand All @@ -39,8 +41,27 @@ func newJavaPlugin(ctx context.Context, config moduleconfig.ModuleConfig) *javaP
}
}

func (p *javaPlugin) CreateModule(ctx context.Context, config moduleconfig.ModuleConfig, includeBinDir bool, replacements map[string]string, group string) error {
func (p *javaPlugin) GetCreateModuleFlags(ctx context.Context) ([]*kong.Flag, error) {
return []*kong.Flag{
{
Value: &kong.Value{
Name: "group",
Help: "The Maven groupId of the project.",
Tag: &kong.Tag{},
HasDefault: true,
Default: "com.example",
},
},
}, nil
}

func (p *javaPlugin) CreateModule(ctx context.Context, projConfig projectconfig.Config, c moduleconfig.ModuleConfig, flags map[string]string) error {
logger := log.FromContext(ctx)
config := c.Abs()
group, ok := flags["group"]
if !ok {
return fmt.Errorf("group flag not set")
}

var source *zip.Reader
if config.Language == "java" {
Expand All @@ -66,7 +87,7 @@ func (p *javaPlugin) CreateModule(ctx context.Context, config moduleconfig.Modul
}

opts := []scaffolder.Option{scaffolder.Exclude("^go.mod$")}
if !includeBinDir {
if !projConfig.Hermit {
logger.Debugf("Excluding bin directory")
opts = append(opts, scaffolder.Exclude("^bin"))
}
Expand Down
8 changes: 7 additions & 1 deletion internal/buildengine/plugin_rust.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"github.com/TBD54566975/ftl/internal/exec"
"github.com/TBD54566975/ftl/internal/log"
"github.com/TBD54566975/ftl/internal/moduleconfig"
"github.com/TBD54566975/ftl/internal/projectconfig"
"github.com/alecthomas/kong"
)

type rustPlugin struct {
Expand All @@ -23,7 +25,11 @@ func newRustPlugin(ctx context.Context, config moduleconfig.ModuleConfig) *rustP
}
}

func (p *rustPlugin) CreateModule(ctx context.Context, config moduleconfig.ModuleConfig, includeBinDir bool, replacements map[string]string, group string) error {
func (p *rustPlugin) GetCreateModuleFlags(ctx context.Context) ([]*kong.Flag, error) {
return []*kong.Flag{}, nil
}

func (p *rustPlugin) CreateModule(ctx context.Context, projConfig projectconfig.Config, c moduleconfig.ModuleConfig, flags map[string]string) error {
return fmt.Errorf("not implemented")
}

Expand Down

0 comments on commit bbb404b

Please sign in to comment.