Skip to content

Commit

Permalink
feat: refactor ftl init and introduce ftl new (#1812)
Browse files Browse the repository at this point in the history
Fixes #1775
  • Loading branch information
safeer authored Jun 18, 2024
1 parent 0fa133e commit a0888c6
Show file tree
Hide file tree
Showing 21 changed files with 589 additions and 141 deletions.
2 changes: 1 addition & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ KT_RUNTIME_RUNNER_TEMPLATE_OUT := "build/template/ftl/jars/ftl-runtime.jar"
RUNNER_TEMPLATE_ZIP := "backend/controller/scaling/localscaling/template.zip"
TIMESTAMP := `date +%s`
SCHEMA_OUT := "backend/protos/xyz/block/ftl/v1/schema/schema.proto"
ZIP_DIRS := "go-runtime/compile/build-template go-runtime/compile/external-module-template go-runtime/scaffolding kotlin-runtime/scaffolding kotlin-runtime/external-module-template"
ZIP_DIRS := "go-runtime/compile/build-template go-runtime/compile/external-module-template common-runtime/scaffolding go-runtime/scaffolding kotlin-runtime/scaffolding kotlin-runtime/external-module-template"
FRONTEND_OUT := "frontend/dist/index.html"
EXTENSION_OUT := "extensions/vscode/dist/extension.js"
PROTOS_IN := "backend/protos/xyz/block/ftl/v1/schema/schema.proto backend/protos/xyz/block/ftl/v1/console/console.proto backend/protos/xyz/block/ftl/v1/ftl.proto backend/protos/xyz/block/ftl/v1/schema/runtime.proto"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ diff -u <(
```sh file=test.sh region=init
mkdir myproject
cd myproject
ftl init go . alice
ftl init .
ftl new go . alice
```

### Build and deploy the module
Expand Down
14 changes: 10 additions & 4 deletions buildengine/watch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ func TestWatch(t *testing.T) {
// Initiate two modules
err := gitInit(dir)
assert.NoError(t, err)
err = ftl("init", "go", dir, "one")
err = ftl("init", dir)
assert.NoError(t, err)
err = ftl("init", "go", dir, "two")
err = ftl("new", "go", dir, "one")
assert.NoError(t, err)
err = ftl("new", "go", dir, "two")
assert.NoError(t, err)

one := loadModule(t, dir, "one")
Expand Down Expand Up @@ -74,7 +76,9 @@ func TestWatchWithBuildModifyingFiles(t *testing.T) {
// Initiate a module
err := gitInit(dir)
assert.NoError(t, err)
err = ftl("init", "go", dir, "one")
err = ftl("init", dir)
assert.NoError(t, err)
err = ftl("new", "go", dir, "one")
assert.NoError(t, err)

events, topic := startWatching(ctx, t, w, dir)
Expand Down Expand Up @@ -109,7 +113,9 @@ func TestWatchWithBuildAndUserModifyingFiles(t *testing.T) {
// Initiate a module
err := gitInit(dir)
assert.NoError(t, err)
err = ftl("init", "go", dir, "one")
err = ftl("init", dir)
assert.NoError(t, err)
err = ftl("new", "go", dir, "one")
assert.NoError(t, err)

one := loadModule(t, dir, "one")
Expand Down
170 changes: 50 additions & 120 deletions cmd/ftl/cmd_init.go
Original file line number Diff line number Diff line change
@@ -1,157 +1,83 @@
package main

import (
"archive/zip"
"bufio"
"context"
"fmt"
"go/token"
"html/template"
"os"
"path"
"path/filepath"
"regexp"
"strings"

"github.com/TBD54566975/scaffolder"

"github.com/TBD54566975/ftl/backend/schema"
"github.com/TBD54566975/ftl/backend/schema/strcase"
"github.com/TBD54566975/ftl/buildengine"
"github.com/TBD54566975/ftl"
commonruntime "github.com/TBD54566975/ftl/common-runtime"
"github.com/TBD54566975/ftl/common/projectconfig"
goruntime "github.com/TBD54566975/ftl/go-runtime"
"github.com/TBD54566975/ftl/internal"
"github.com/TBD54566975/ftl/internal/exec"
"github.com/TBD54566975/ftl/internal/log"
kotlinruntime "github.com/TBD54566975/ftl/kotlin-runtime"
)

type initCmd struct {
Hermit bool `help:"Include Hermit language-specific toolchain binaries in the module." negatable:""`
Go initGoCmd `cmd:"" help:"Initialize a new FTL Go module."`
Kotlin initKotlinCmd `cmd:"" help:"Initialize a new FTL Kotlin module."`
}

type initGoCmd struct {
Replace map[string]string `short:"r" help:"Replace a module import path with a local path in the initialised FTL module." placeholder:"OLD=NEW,..." env:"FTL_INIT_GO_REPLACE"`
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."`
Hermit bool `help:"Include Hermit language-specific toolchain binaries." negatable:""`
Dir string `arg:"" help:"Directory to initialize the project in."`
ExternalDirs []string `help:"Directories of existing external modules."`
ModuleDirs []string `help:"Child directories of existing modules."`
NoGit bool `help:"Don't add files to the git repository."`
Startup string `help:"Command to run on startup."`
}

func isValidModuleName(name string) bool {
validNamePattern := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*$`)
if !validNamePattern.MatchString(name) {
return false
}
if token.Lookup(name).IsKeyword() {
return false
}
return true
}

func (i initGoCmd) Run(ctx context.Context, parent *initCmd) error {
if i.Name == "" {
i.Name = filepath.Base(i.Dir)
}

// Validate the module name with custom validation
if !isValidModuleName(i.Name) {
return fmt.Errorf("module name %q must be a valid Go module name and not a reserved keyword", i.Name)
}

if !schema.ValidateName(i.Name) {
return fmt.Errorf("module name %q is invalid", i.Name)
}

if _, ok := internal.GitRoot(i.Dir).Get(); !ok {
return fmt.Errorf("directory %s is not in a git repository, run 'git init' at the root of your project", i.Dir)
func (i initCmd) Run(ctx context.Context) error {
if i.Dir == "" {
return fmt.Errorf("directory is required")
}

logger := log.FromContext(ctx)
logger.Debugf("Initializing FTL Go module %s in %s", i.Name, i.Dir)
if err := scaffold(parent.Hermit, goruntime.Files(), i.Dir, i, scaffolder.Exclude("^go.mod$")); err != nil {
return err
}
if err := updateGitIgnore(i.Dir); err != nil {
return err
}
if err := projectconfig.MaybeCreateDefault(ctx); err != nil {
logger.Debugf("Initializing FTL project in %s", i.Dir)
if err := scaffold(ctx, i.Hermit, commonruntime.Files(), i.Dir, i); err != nil {
return err
}
logger.Debugf("Running go mod tidy")
return exec.Command(ctx, log.Debug, filepath.Join(i.Dir, i.Name), "go", "mod", "tidy").RunBuffered(ctx)
}

type initKotlinCmd struct {
GroupID string `short:"g" help:"Base Maven group ID (defaults to \"ftl\")." default:"ftl"`
ArtifactID string `short:"a" help:"Base Maven artifact ID (defaults to \"ftl\")." default:"ftl"`
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."`
}

func (i initKotlinCmd) Run(ctx context.Context, parent *initCmd) error {
if i.Name == "" {
i.Name = filepath.Base(i.Dir)
}

if !schema.ValidateName(i.Name) {
return fmt.Errorf("module name %q is invalid", i.Name)
}

moduleDir := filepath.Join(i.Dir, i.Name)
if _, err := os.Stat(moduleDir); err == nil {
return fmt.Errorf("module directory %s already exists", filepath.Join(i.Dir, i.Name))
config := projectconfig.Config{
Hermit: i.Hermit,
NoGit: i.NoGit,
FTLMinVersion: ftl.Version,
ExternalDirs: i.ExternalDirs,
ModuleDirs: i.ModuleDirs,
Commands: projectconfig.Commands{
Startup: []string{i.Startup},
},
}

if err := scaffold(parent.Hermit, kotlinruntime.Files(), i.Dir, i); err != nil {
if err := projectconfig.Create(ctx, config, i.Dir); err != nil {
return err
}

return buildengine.SetPOMProperties(ctx, moduleDir)
}

func unzipToTmpDir(reader *zip.Reader) (string, error) {
tmpDir, err := os.MkdirTemp("", "ftl-init-*")
if err != nil {
return "", err
}
err = internal.UnzipDir(reader, tmpDir)
if err != nil {
return "", err
gitRoot, ok := internal.GitRoot(i.Dir).Get()
if !i.NoGit && ok {
logger.Debugf("Updating .gitignore")
if err := updateGitIgnore(ctx, gitRoot); err != nil {
return err
}
logger.Debugf("Adding files to git")
if i.Hermit {
if err := maybeGitAdd(ctx, i.Dir, "bin/*"); err != nil {
return err
}
}
if err := maybeGitAdd(ctx, i.Dir, "ftl-project.toml"); err != nil {
return err
}
}
return tmpDir, nil
return nil
}

func scaffold(hermit bool, source *zip.Reader, destination string, ctx any, options ...scaffolder.Option) error {
opts := []scaffolder.Option{scaffolder.Functions(scaffoldFuncs), scaffolder.Exclude("^go.mod$")}
if !hermit {
opts = append(opts, scaffolder.Exclude("^bin"))
}
opts = append(opts, options...)
if err := internal.ScaffoldZip(source, destination, ctx, opts...); err != nil {
return fmt.Errorf("failed to scaffold: %w", err)
func maybeGitAdd(ctx context.Context, dir string, paths ...string) error {
args := append([]string{"add"}, paths...)
if err := exec.Command(ctx, log.Debug, dir, "git", args...).RunBuffered(ctx); err != nil {
return err
}
return nil
}

var scaffoldFuncs = template.FuncMap{
"snake": strcase.ToLowerSnake,
"screamingSnake": strcase.ToUpperSnake,
"camel": strcase.ToUpperCamel,
"lowerCamel": strcase.ToLowerCamel,
"kebab": strcase.ToLowerKebab,
"screamingKebab": strcase.ToUpperKebab,
"upper": strings.ToUpper,
"lower": strings.ToLower,
"title": strings.Title,
"typename": schema.TypeName,
}

func updateGitIgnore(dir string) error {
gitRoot, ok := internal.GitRoot(dir).Get()
if !ok {
return nil
}
func updateGitIgnore(ctx context.Context, gitRoot string) error {
f, err := os.OpenFile(path.Join(gitRoot, ".gitignore"), os.O_RDWR|os.O_CREATE, 0644) //nolint:gosec
if err != nil {
return err
Expand All @@ -170,6 +96,10 @@ func updateGitIgnore(dir string) error {
}

// append if not already present
_, err = f.WriteString("**/_ftl\n")
return err
if _, err = f.WriteString("**/_ftl\n"); err != nil {
return err
}

// Add .gitignore to git
return maybeGitAdd(ctx, gitRoot, ".gitignore")
}
Loading

0 comments on commit a0888c6

Please sign in to comment.