From 71fde020fbf0c60fcd20dab4e4d5b6a9cc7260aa Mon Sep 17 00:00:00 2001 From: Denise Li Date: Wed, 5 Jun 2024 14:03:40 -0400 Subject: [PATCH] feat: generate ftl-project.toml in the Git root if it DNE (#1648) Fixes https://github.com/TBD54566975/ftl/issues/1639 The current implementation ignores any `--config` flag values, but let me know if we should default to creating the last _specified_ config whenever one's provided. For that, I'd just move this code to the resolver and add an `r.Config` check. I decided to minimize it purely to operating on the default path because the specified paths may not actually be at the Git root, which could make the new behavior a bit more complex to comprehend. That said, there could be a good reason to use the flag value that I didn't think of. --- .../controller/scaling/localscaling/devel.go | 11 ++++- buildengine/walk.go | 4 +- cmd/ftl/cmd_init.go | 10 ++++- .../projectconfig_resolver_test.go | 3 +- common/projectconfig/projectconfig.go | 40 ++++++++++++++++--- frontend/local.go | 8 +++- .../ftl/ftltest/ftltest_integration_test.go | 3 +- integration/actions.go | 9 +++++ integration/harness.go | 3 +- internal/source_root.go | 10 +++-- 10 files changed, 81 insertions(+), 20 deletions(-) diff --git a/backend/controller/scaling/localscaling/devel.go b/backend/controller/scaling/localscaling/devel.go index 8a0f4d5ace..7439a3e732 100644 --- a/backend/controller/scaling/localscaling/devel.go +++ b/backend/controller/scaling/localscaling/devel.go @@ -15,12 +15,19 @@ import ( var templateDirOnce sync.Once func templateDir(ctx context.Context) string { + gitRoot, ok := internal.GitRoot("").Get() + if !ok { + // If GitRoot encounters an error, it will fail to find the correct dir. + // This line preserves the original behavior to prevent a regression, but + // it is still not the desired outcome. More thinking needed. + gitRoot = "" + } templateDirOnce.Do(func() { // TODO: Figure out how to make maven build offline - err := exec.Command(ctx, log.Debug, internal.GitRoot(""), "just", "build-kt-runtime").RunBuffered(ctx) + err := exec.Command(ctx, log.Debug, gitRoot, "just", "build-kt-runtime").RunBuffered(ctx) if err != nil { panic(err) } }) - return filepath.Join(internal.GitRoot(""), "build/template") + return filepath.Join(gitRoot, "build/template") } diff --git a/buildengine/walk.go b/buildengine/walk.go index beda919931..ec4d5d47e4 100644 --- a/buildengine/walk.go +++ b/buildengine/walk.go @@ -102,8 +102,8 @@ func initGitIgnore(dir string) []string { if err == nil { ignore = append(ignore, loadGitIgnore(home)...) } - gitRoot := internal.GitRoot(dir) - if gitRoot != "" { + gitRoot, ok := internal.GitRoot(dir).Get() + if ok { for current := dir; strings.HasPrefix(current, gitRoot); current = path.Dir(current) { ignore = append(ignore, loadGitIgnore(current)...) } diff --git a/cmd/ftl/cmd_init.go b/cmd/ftl/cmd_init.go index 6190d87143..b643a0dcbb 100644 --- a/cmd/ftl/cmd_init.go +++ b/cmd/ftl/cmd_init.go @@ -16,6 +16,7 @@ import ( "github.com/TBD54566975/ftl/backend/schema" "github.com/TBD54566975/ftl/backend/schema/strcase" "github.com/TBD54566975/ftl/buildengine" + "github.com/TBD54566975/ftl/common/projectconfig" goruntime "github.com/TBD54566975/ftl/go-runtime" "github.com/TBD54566975/ftl/internal" "github.com/TBD54566975/ftl/internal/exec" @@ -50,8 +51,10 @@ func (i initGoCmd) Run(ctx context.Context, parent *initCmd) error { if err := updateGitIgnore(i.Dir); err != nil { return err } + if err := projectconfig.CreateDefaultFileIfNonexistent(ctx); 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) } @@ -121,7 +124,10 @@ var scaffoldFuncs = template.FuncMap{ } func updateGitIgnore(dir string) error { - gitRoot := internal.GitRoot(dir) + gitRoot, ok := internal.GitRoot(dir).Get() + if !ok { + return nil + } f, err := os.OpenFile(path.Join(gitRoot, ".gitignore"), os.O_RDWR|os.O_CREATE, 0644) //nolint:gosec if err != nil { return err diff --git a/common/configuration/projectconfig_resolver_test.go b/common/configuration/projectconfig_resolver_test.go index c1a0b7ed2e..dc5e3eeec5 100644 --- a/common/configuration/projectconfig_resolver_test.go +++ b/common/configuration/projectconfig_resolver_test.go @@ -15,7 +15,8 @@ import ( ) func TestSet(t *testing.T) { - defaultPath := projectconfig.GetDefaultConfigPath() + defaultPath, ok := projectconfig.DefaultConfigPath().Get() + assert.True(t, ok) origConfigBytes, err := os.ReadFile(defaultPath) assert.NoError(t, err) diff --git a/common/projectconfig/projectconfig.go b/common/projectconfig/projectconfig.go index 936419b8e2..0b598af666 100644 --- a/common/projectconfig/projectconfig.go +++ b/common/projectconfig/projectconfig.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/BurntSushi/toml" + "github.com/alecthomas/types/optional" "github.com/TBD54566975/ftl" "github.com/TBD54566975/ftl/internal" @@ -43,16 +44,43 @@ func ConfigPaths(input []string) []string { if len(input) > 0 { return input } - path := GetDefaultConfigPath() + path, ok := DefaultConfigPath().Get() + if !ok { + return []string{} + } _, err := os.Stat(path) - if err == nil { - return []string{path} + if err != nil { + return []string{} } - return []string{} + return []string{path} } -func GetDefaultConfigPath() string { - return filepath.Join(internal.GitRoot(""), "ftl-project.toml") +func DefaultConfigPath() optional.Option[string] { + gitRoot, ok := internal.GitRoot("").Get() + if !ok { + return optional.None[string]() + } + return optional.Some(filepath.Join(gitRoot, "ftl-project.toml")) +} + +// CreateDefaultFileIfNonexistent creates the ftl-project.toml file in the Git root if it +// does not already exist. +func CreateDefaultFileIfNonexistent(ctx context.Context) error { + logger := log.FromContext(ctx) + path, ok := DefaultConfigPath().Get() + if !ok { + logger.Warnf("Failed to find Git root, so cannot verify whether an ftl-project.toml file exists there") + return nil + } + _, err := os.Stat(path) + if err == nil { + return nil + } + if !errors.Is(err, os.ErrNotExist) { + return err + } + logger.Warnf("Creating a new project config file at %q because the file does not already exist", path) + return Save(path, Config{}) } func LoadConfig(ctx context.Context, input []string) (Config, error) { diff --git a/frontend/local.go b/frontend/local.go index 9aad2f685b..2e4def35f0 100644 --- a/frontend/local.go +++ b/frontend/local.go @@ -4,6 +4,7 @@ package frontend import ( "context" + "fmt" "net/http" "net/http/httputil" "net/url" @@ -22,7 +23,12 @@ func Server(ctx context.Context, timestamp time.Time, publicURL *url.URL, allowO logger := log.FromContext(ctx) logger.Debugf("Building console...") - err := exec.Command(ctx, log.Debug, internal.GitRoot(""), "just", "build-frontend").RunBuffered(ctx) + gitRoot, ok := internal.GitRoot("").Get() + if !ok { + return nil, fmt.Errorf("failed to find Git root") + } + + err := exec.Command(ctx, log.Debug, gitRoot, "just", "build-frontend").RunBuffered(ctx) if err != nil { return nil, err } diff --git a/go-runtime/ftl/ftltest/ftltest_integration_test.go b/go-runtime/ftl/ftltest/ftltest_integration_test.go index c6665019c7..63d6983b94 100644 --- a/go-runtime/ftl/ftltest/ftltest_integration_test.go +++ b/go-runtime/ftl/ftltest/ftltest_integration_test.go @@ -9,7 +9,8 @@ import ( ) func TestModuleUnitTests(t *testing.T) { - in.Run(t, "", + in.Run(t, "wrapped/ftl-project.toml", + in.GitInit(), in.CopyModule("time"), in.CopyModule("wrapped"), in.CopyModule("verbtypes"), diff --git a/integration/actions.go b/integration/actions.go index fec8777ee1..ec9879c383 100644 --- a/integration/actions.go +++ b/integration/actions.go @@ -38,6 +38,15 @@ func Scaffold(src, dest string, tmplCtx any) Action { } } +// GitInit calls git init on the working directory. +func GitInit() Action { + return func(t testing.TB, ic TestContext) { + Infof("Running `git init` on the working directory: %s", ic.workDir) + err := ftlexec.Command(ic, log.Debug, ic.workDir, "git", "init", ic.workDir).RunBuffered(ic) + assert.NoError(t, err) + } +} + // Copy a module from the testdata directory to the working directory. // // Ensures that replace directives are correctly handled. diff --git a/integration/harness.go b/integration/harness.go index 07d5f8e301..43802f4d0a 100644 --- a/integration/harness.go +++ b/integration/harness.go @@ -54,7 +54,8 @@ func Run(t *testing.T, ftlConfigPath string, actions ...Action) { cwd, err := os.Getwd() assert.NoError(t, err) - rootDir := internal.GitRoot("") + rootDir, ok := internal.GitRoot("").Get() + assert.True(t, ok) if ftlConfigPath != "" { // Use a path into the testdata directory instead of one relative to diff --git a/internal/source_root.go b/internal/source_root.go index df5f4bfe71..4952a3b2eb 100644 --- a/internal/source_root.go +++ b/internal/source_root.go @@ -4,24 +4,26 @@ import ( "os" "os/exec" //nolint:depguard "strings" + + "github.com/alecthomas/types/optional" ) // GitRoot returns the root of the git repository containing dir, or empty string if dir is not in a git repository. // // If dir is empty, the current working directory is used. -func GitRoot(dir string) string { +func GitRoot(dir string) optional.Option[string] { if dir == "" { var err error dir, err = os.Getwd() if err != nil { - return "" + return optional.None[string]() } } cmd := exec.Command("git", "rev-parse", "--show-toplevel") cmd.Dir = dir output, err := cmd.CombinedOutput() if err != nil { - return "" + return optional.None[string]() } - return strings.TrimSpace(string(output)) + return optional.Some(strings.TrimSpace(string(output))) }