diff --git a/buildengine/build.go b/buildengine/build.go index 6b83b8d3d1..e644ef54cb 100644 --- a/buildengine/build.go +++ b/buildengine/build.go @@ -3,36 +3,145 @@ package buildengine import ( "context" "fmt" + "os" + "path/filepath" "github.com/TBD54566975/ftl/backend/schema" "github.com/TBD54566975/ftl/common/moduleconfig" "github.com/TBD54566975/ftl/internal/log" ) -// A Module is a ModuleConfig with its dependencies populated. +// A Module is a ModuleConfig or ExternalLibrary with its dependencies populated. type Module struct { - moduleconfig.ModuleConfig + internal interface{} Dependencies []string } +type ExternalLibrary struct { + Dir string + Language string +} + +func (m Module) Key() string { + switch module := m.internal.(type) { + case moduleconfig.ModuleConfig: + return module.Module + case ExternalLibrary: + return module.Dir + default: + panic(fmt.Sprintf("unknown internal type %T", m.internal)) + } +} + +func (m Module) Language() string { + switch module := m.internal.(type) { + case moduleconfig.ModuleConfig: + return module.Language + case ExternalLibrary: + return module.Language + default: + panic(fmt.Sprintf("unknown internal type %T", m.internal)) + } +} + +func (m Module) Dir() string { + switch module := m.internal.(type) { + case moduleconfig.ModuleConfig: + return module.Dir + case ExternalLibrary: + return module.Dir + default: + panic(fmt.Sprintf("unknown internal type %T", m.internal)) + } +} + +func (m Module) Watch() []string { + switch module := m.internal.(type) { + case moduleconfig.ModuleConfig: + return module.Watch + case ExternalLibrary: + switch module.Language { + case "go": + return []string{"**/*.go", "go.mod", "go.sum"} + case "kotlin": + return []string{"pom.xml", "src/**", "target/generated-sources"} + default: + panic(fmt.Sprintf("unknown language %T", m.Language())) + } + default: + panic(fmt.Sprintf("unknown internal type %T", m.internal)) + } +} + +func (m Module) Kind() string { + switch m.internal.(type) { + case moduleconfig.ModuleConfig: + return "module" + case ExternalLibrary: + return "library" + default: + panic(fmt.Sprintf("unknown internal type %T", m.internal)) + } +} + +func (m Module) ModuleConfig() (moduleconfig.ModuleConfig, bool) { + config, ok := m.internal.(moduleconfig.ModuleConfig) + return config, ok +} + +func (m Module) ExternalLibrary() (ExternalLibrary, bool) { + lib, ok := m.internal.(ExternalLibrary) + return lib, ok +} + // LoadModule loads a module from the given directory. -// -// A [Module] includes the module configuration as well as its dependencies -// extracted from source code. func LoadModule(ctx context.Context, dir string) (Module, error) { config, err := moduleconfig.LoadModuleConfig(dir) if err != nil { return Module{}, err } - return UpdateDependencies(ctx, config) + module := Module{internal: config} + return module, nil +} + +func LoadExternalLibrary(ctx context.Context, dir string) (Module, error) { + lib := ExternalLibrary{ + Dir: dir, + } + + goModPath := filepath.Join(dir, "go.mod") + pomPath := filepath.Join(dir, "pom.xml") + if _, err := os.Stat(goModPath); err == nil { + lib.Language = "go" + } else if !os.IsNotExist(err) { + return Module{}, err + } else { + if _, err = os.Stat(pomPath); err == nil { + lib.Language = "kotlin" + } else if !os.IsNotExist(err) { + return Module{}, err + } + } + if lib.Language == "" { + return Module{}, fmt.Errorf("could not autodetect language: no go.mod or pom.xml found in %s", dir) + } + + module := Module{ + internal: lib, + } + return module, nil } // Build a module in the given directory given the schema and module config. func Build(ctx context.Context, sch *schema.Schema, module Module) error { - logger := log.FromContext(ctx).Scope(module.Module) + config, ok := module.ModuleConfig() + if !ok { + return fmt.Errorf("cannot build module without module config: %q", module.Key()) + } + logger := log.FromContext(ctx).Scope(config.Module) ctx = log.ContextWithLogger(ctx, logger) logger.Infof("Building module") - switch module.Language { + switch config.Language { case "go": return buildGo(ctx, sch, module) @@ -40,6 +149,6 @@ func Build(ctx context.Context, sch *schema.Schema, module Module) error { return buildKotlin(ctx, sch, module) default: - return fmt.Errorf("unknown language %q", module.Language) + return fmt.Errorf("unknown language %q", config.Language) } } diff --git a/buildengine/build_go.go b/buildengine/build_go.go index 31f1d31e75..3c05afbc84 100644 --- a/buildengine/build_go.go +++ b/buildengine/build_go.go @@ -9,8 +9,12 @@ import ( ) func buildGo(ctx context.Context, sch *schema.Schema, module Module) error { - if err := compile.Build(ctx, module.Dir, sch); err != nil { - return fmt.Errorf("failed to build module %s: %w", module.Module, err) + moduleConfig, ok := module.ModuleConfig() + if !ok { + return fmt.Errorf("module %s is not a FTL module", module.Key()) + } + if err := compile.Build(ctx, moduleConfig.Dir, sch); err != nil { + return fmt.Errorf("failed to build module %s: %w", moduleConfig.Module, err) } return nil } diff --git a/buildengine/build_kotlin.go b/buildengine/build_kotlin.go index d23fa2c546..2524ee1a53 100644 --- a/buildengine/build_kotlin.go +++ b/buildengine/build_kotlin.go @@ -16,6 +16,7 @@ import ( "github.com/TBD54566975/ftl" "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/common/moduleconfig" "github.com/TBD54566975/ftl/internal" "github.com/TBD54566975/ftl/internal/exec" "github.com/TBD54566975/ftl/internal/log" @@ -28,9 +29,13 @@ type externalModuleContext struct { } func (e externalModuleContext) ExternalModules() []*schema.Module { + name := "" + if config, ok := e.module.ModuleConfig(); ok { + name = config.Module + } modules := make([]*schema.Module, 0, len(e.Modules)) for _, module := range e.Modules { - if module.Name == e.module.Module { + if module.Name == name { continue } modules = append(modules, module) @@ -39,23 +44,28 @@ func (e externalModuleContext) ExternalModules() []*schema.Module { } func buildKotlin(ctx context.Context, sch *schema.Schema, module Module) error { + config, ok := module.ModuleConfig() + if !ok { + return fmt.Errorf("module %s is not a FTL module", module.Key()) + } + logger := log.FromContext(ctx) - if err := SetPOMProperties(ctx, module.Dir); err != nil { + if err := SetPOMProperties(ctx, config.Dir); err != nil { return fmt.Errorf("unable to update ftl.version in %s: %w", module.Dir, err) } if err := generateExternalModules(ctx, module, sch); err != nil { - return fmt.Errorf("unable to generate external modules for %s: %w", module.Module, err) + return fmt.Errorf("unable to generate external modules for %s: %w", config.Module, err) } - if err := prepareFTLRoot(module); err != nil { - return fmt.Errorf("unable to prepare FTL root for %s: %w", module.Module, err) + if err := prepareFTLRoot(config); err != nil { + return fmt.Errorf("unable to prepare FTL root for %s: %w", config.Module, err) } - logger.Debugf("Using build command '%s'", module.Build) - err := exec.Command(ctx, log.Debug, module.Dir, "bash", "-c", module.Build).RunBuffered(ctx) + logger.Debugf("Using build command '%s'", config.Build) + err := exec.Command(ctx, log.Debug, config.Dir, "bash", "-c", config.Build).RunBuffered(ctx) if err != nil { - return fmt.Errorf("failed to build module %s: %w", module.Module, err) + return fmt.Errorf("failed to build module %s: %w", config.Module, err) } return nil @@ -92,8 +102,8 @@ func SetPOMProperties(ctx context.Context, baseDir string) error { return tree.WriteToFile(pomFile) } -func prepareFTLRoot(module Module) error { - buildDir := module.AbsDeployDir() +func prepareFTLRoot(config moduleconfig.ModuleConfig) error { + buildDir := config.AbsDeployDir() if err := os.MkdirAll(buildDir, 0700); err != nil { return err } @@ -107,7 +117,7 @@ SchemaExtractorRuleSet: detektYmlPath := filepath.Join(buildDir, "detekt.yml") if err := os.WriteFile(detektYmlPath, []byte(fileContent), 0600); err != nil { - return fmt.Errorf("unable to configure detekt for %s: %w", module.Module, err) + return fmt.Errorf("unable to configure detekt for %s: %w", config.Module, err) } mainFilePath := filepath.Join(buildDir, "main") @@ -116,21 +126,20 @@ SchemaExtractorRuleSet: exec java -cp "classes:$(cat classpath.txt)" xyz.block.ftl.main.MainKt ` if err := os.WriteFile(mainFilePath, []byte(mainFile), 0700); err != nil { //nolint:gosec - return fmt.Errorf("unable to configure main executable for %s: %w", module.Module, err) + return fmt.Errorf("unable to configure main executable for %s: %w", config.Module, err) } return nil } func generateExternalModules(ctx context.Context, module Module, sch *schema.Schema) error { logger := log.FromContext(ctx) - config := module.ModuleConfig funcs := maps.Clone(scaffoldFuncs) // Wipe the modules directory to ensure we don't have any stale modules. - _ = os.RemoveAll(filepath.Join(config.Dir, "target", "generated-sources", "ftl")) + _ = os.RemoveAll(filepath.Join(module.Dir(), "target", "generated-sources", "ftl")) logger.Debugf("Generating external modules") - return internal.ScaffoldZip(kotlinruntime.ExternalModuleTemplates(), config.Dir, externalModuleContext{ + return internal.ScaffoldZip(kotlinruntime.ExternalModuleTemplates(), module.Dir(), externalModuleContext{ module: module, Schema: sch, }, scaffolder.Exclude("^go.mod$"), scaffolder.Functions(funcs)) diff --git a/buildengine/deploy.go b/buildengine/deploy.go index 59fd6b4742..c6f9cfafa3 100644 --- a/buildengine/deploy.go +++ b/buildengine/deploy.go @@ -36,12 +36,17 @@ type DeployClient interface { // Deploy a module to the FTL controller with the given number of replicas. Optionally wait for the deployment to become ready. func Deploy(ctx context.Context, module Module, replicas int32, waitForDeployOnline bool, client DeployClient) error { - logger := log.FromContext(ctx).Scope(module.Module) + moduleConfig, ok := module.ModuleConfig() + if !ok { + return fmt.Errorf("can not deploy non-FTL module %s", module.Key()) + } + + logger := log.FromContext(ctx).Scope(moduleConfig.Module) ctx = log.ContextWithLogger(ctx, logger) logger.Infof("Deploying module") - deployDir := module.AbsDeployDir() - files, err := findFiles(deployDir, module.Deploy) + deployDir := moduleConfig.AbsDeployDir() + files, err := findFiles(deployDir, moduleConfig.Deploy) if err != nil { logger.Errorf(err, "failed to find files in %s", deployDir) return err @@ -57,9 +62,9 @@ func Deploy(ctx context.Context, module Module, replicas int32, waitForDeployOnl return err } - moduleSchema, err := loadProtoSchema(deployDir, module.ModuleConfig, replicas) + moduleSchema, err := loadProtoSchema(deployDir, moduleConfig, replicas) if err != nil { - return fmt.Errorf("failed to load protobuf schema from %q: %w", module.ModuleConfig.Schema, err) + return fmt.Errorf("failed to load protobuf schema from %q: %w", moduleConfig.Schema, err) } logger.Debugf("Uploading %d/%d files", len(gadResp.Msg.MissingDigests), len(files)) diff --git a/buildengine/deps.go b/buildengine/deps.go index c86964ae41..d994169d51 100644 --- a/buildengine/deps.go +++ b/buildengine/deps.go @@ -17,12 +17,11 @@ import ( "golang.design/x/reflect" "golang.org/x/exp/maps" - "github.com/TBD54566975/ftl/common/moduleconfig" "github.com/TBD54566975/ftl/internal/log" ) // UpdateAllDependencies calls UpdateDependencies on each module in the list. -func UpdateAllDependencies(ctx context.Context, modules ...moduleconfig.ModuleConfig) ([]Module, error) { +func UpdateAllDependencies(ctx context.Context, modules ...Module) ([]Module, error) { out := []Module{} for _, module := range modules { updated, err := UpdateDependencies(ctx, module) @@ -34,12 +33,12 @@ func UpdateAllDependencies(ctx context.Context, modules ...moduleconfig.ModuleCo return out, nil } -// UpdateDependencies finds the dependencies for an FTL module and returns a +// UpdateDependencies finds the dependencies for a module and returns a // Module with those dependencies populated. -func UpdateDependencies(ctx context.Context, config moduleconfig.ModuleConfig) (Module, error) { +func UpdateDependencies(ctx context.Context, module Module) (Module, error) { logger := log.FromContext(ctx) - logger.Debugf("Extracting dependencies for module %s", config.Module) - dependencies, err := extractDependencies(config) + logger.Debugf("Extracting dependencies for module %s", module.Key()) + dependencies, err := extractDependencies(module) if err != nil { return Module{}, err } @@ -53,20 +52,26 @@ func UpdateDependencies(ctx context.Context, config moduleconfig.ModuleConfig) ( if !containsBuiltin { dependencies = append(dependencies, "builtin") } - out := reflect.DeepCopy(config) - return Module{ModuleConfig: out, Dependencies: dependencies}, nil + + out := reflect.DeepCopy(module) + out.Dependencies = dependencies + return out, nil } -func extractDependencies(config moduleconfig.ModuleConfig) ([]string, error) { - switch config.Language { +func extractDependencies(module Module) ([]string, error) { + name := "" + if config, ok := module.ModuleConfig(); ok { + name = config.Module + } + switch module.Language() { case "go": - return extractGoFTLImports(config.Module, config.Dir) + return extractGoFTLImports(name, module.Dir()) case "kotlin": - return extractKotlinFTLImports(config.Module, config.Dir) + return extractKotlinFTLImports(name, module.Dir()) default: - return nil, fmt.Errorf("unsupported language: %s", config.Language) + return nil, fmt.Errorf("unsupported language: %s", module.Language()) } } diff --git a/buildengine/discover.go b/buildengine/discover.go index 49985a8c44..0354bc907a 100644 --- a/buildengine/discover.go +++ b/buildengine/discover.go @@ -6,14 +6,12 @@ import ( "os" "path/filepath" "sort" - - "github.com/TBD54566975/ftl/common/moduleconfig" ) // DiscoverModules recursively loads all modules under the given directories. // // If no directories are provided, the current working directory is used. -func DiscoverModules(ctx context.Context, dirs ...string) ([]moduleconfig.ModuleConfig, error) { +func DiscoverModules(ctx context.Context, dirs ...string) ([]Module, error) { if len(dirs) == 0 { cwd, err := os.Getwd() if err != nil { @@ -21,18 +19,18 @@ func DiscoverModules(ctx context.Context, dirs ...string) ([]moduleconfig.Module } dirs = []string{cwd} } - out := []moduleconfig.ModuleConfig{} + out := []Module{} for _, dir := range dirs { err := WalkDir(dir, func(path string, d fs.DirEntry) error { if filepath.Base(path) != "ftl.toml" { return nil } moduleDir := filepath.Dir(path) - config, err := moduleconfig.LoadModuleConfig(moduleDir) + module, err := LoadModule(ctx, moduleDir) if err != nil { return err } - out = append(out, config) + out = append(out, module) return ErrSkip }) if err != nil { @@ -40,7 +38,7 @@ func DiscoverModules(ctx context.Context, dirs ...string) ([]moduleconfig.Module } } sort.Slice(out, func(i, j int) bool { - return out[i].Module < out[j].Module + return out[i].Key() < out[j].Key() }) return out, nil } diff --git a/buildengine/engine.go b/buildengine/engine.go index 05f297d5f1..11318d724e 100644 --- a/buildengine/engine.go +++ b/buildengine/engine.go @@ -21,7 +21,6 @@ import ( ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" "github.com/TBD54566975/ftl/backend/schema" - "github.com/TBD54566975/ftl/common/moduleconfig" "github.com/TBD54566975/ftl/go-runtime/compile" "github.com/TBD54566975/ftl/internal/log" "github.com/TBD54566975/ftl/internal/rpc" @@ -76,23 +75,23 @@ func New(ctx context.Context, client ftlv1connect.ControllerServiceClient, dirs e.controllerSchema.Store("builtin", schema.Builtins()) ctx, cancel := context.WithCancel(ctx) e.cancel = cancel - configs, err := DiscoverModules(ctx, dirs...) + modules, err := DiscoverModules(ctx, dirs...) if err != nil { return nil, err } for _, dir := range externalDirs { - externalLib, err := moduleconfig.LoadExternalLibraryConfig(dir) + module, err := LoadExternalLibrary(ctx, dir) if err != nil { return nil, err } - configs = append(configs, externalLib) - } - modules, err := UpdateAllDependencies(ctx, configs...) - if err != nil { - return nil, err + modules = append(modules, module) } for _, module := range modules { - e.modules[module.Module] = module + module, err = UpdateDependencies(ctx, module) + if err != nil { + return nil, err + } + e.modules[module.Key()] = module } if client == nil { @@ -246,25 +245,29 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration case event := <-watchEvents: switch event := event.(type) { case WatchEventModuleAdded: - if _, exists := e.modules[event.Module.Module]; !exists { - e.modules[event.Module.Module] = event.Module - err := e.buildAndDeploy(ctx, 1, true, event.Module.Module) + if _, exists := e.modules[event.Module.Key()]; !exists { + e.modules[event.Module.Key()] = event.Module + err := e.buildAndDeploy(ctx, 1, true, event.Module.Key()) if err != nil { - logger.Errorf(err, "deploy %s failed", event.Module.Module) + logger.Errorf(err, "deploy %s failed", event.Module.Key()) } } case WatchEventModuleRemoved: - //TODO: no need to terminate if ext lib - err := teminateModuleDeployment(ctx, e.client, event.Module.Module) - if err != nil { - logger.Errorf(err, "terminate %s failed", event.Module.Module) + if _, ok := event.Module.ModuleConfig(); ok { + err := teminateModuleDeployment(ctx, e.client, event.Module.Key()) + if err != nil { + logger.Errorf(err, "terminate %s failed", event.Module.Key()) + } } - delete(e.modules, event.Module.Module) + delete(e.modules, event.Module.Key()) case WatchEventModuleChanged: - err := e.buildAndDeploy(ctx, 1, true, event.Module.Module) + err := e.buildAndDeploy(ctx, 1, true, event.Module.Key()) if err != nil { - //TODO: log message inaccurate for ext libs - logger.Errorf(err, "deploy %s failed", event.Module.Module) + if _, ok := event.Module.ModuleConfig(); ok { + logger.Errorf(err, "deploy %s failed", event.Module.Key()) + } else { + logger.Errorf(err, "stub generation for %s failed", event.Module.Key()) + } } } case change := <-schemaChanges: @@ -336,16 +339,13 @@ func (e *Engine) buildAndDeploy(ctx context.Context, replicas int32, waitForDepl //TODO: don't always include external libs return e.buildWithCallback(ctx, func(ctx context.Context, module Module) error { - switch module.Type { - case moduleconfig.FTL: + if _, ok := module.ModuleConfig(); ok { select { case deployQueue <- module: return nil case <-ctx.Done(): return ctx.Err() } - case moduleconfig.ExternalLibrary: - } return nil }, modules...) @@ -360,12 +360,10 @@ func (e *Engine) buildAndDeploy(ctx context.Context, replicas int32, waitForDepl if !ok { return nil } - switch module.Type { - case moduleconfig.FTL: + if _, ok = module.ModuleConfig(); ok { if err := Deploy(ctx, module, replicas, waitForDeployOnline, e.client); err != nil { return err } - case moduleconfig.ExternalLibrary: } case <-ctx.Done(): return ctx.Err() @@ -391,7 +389,7 @@ func (e *Engine) buildWithCallback(ctx context.Context, callback buildCallback, } // Update dependencies before building. var err error - module, err = UpdateDependencies(ctx, module.ModuleConfig) + module, err = UpdateDependencies(ctx, module) if err != nil { return err } @@ -412,22 +410,21 @@ func (e *Engine) buildWithCallback(ctx context.Context, callback buildCallback, schemas := make(chan *schema.Module, len(group)) wg, ctx := errgroup.WithContext(ctx) wg.SetLimit(e.parallelism) - for _, name := range group { + for _, key := range group { wg.Go(func() error { - if !mustBuild[name] { - return e.mustSchema(ctx, name, built, schemas) + if !mustBuild[key] { + return e.mustSchema(ctx, key, built, schemas) } - switch e.modules[name].Type { - case moduleconfig.FTL: - err := e.build(ctx, name, built, schemas) + if _, ok := e.modules[key].ModuleConfig(); ok { + err := e.build(ctx, key, built, schemas) if err == nil && callback != nil { - return callback(ctx, e.modules[name]) + return callback(ctx, e.modules[key]) } return err - case moduleconfig.ExternalLibrary: - if err := e.generateStubsForExternalLib(ctx, name, built); err != nil { - return fmt.Errorf("could not build stubs for external library %s: %w", name, err) + } else if _, ok := e.modules[key].ExternalLibrary(); ok { + if err := e.generateStubsForExternalLib(ctx, key, built); err != nil { + return fmt.Errorf("could not build stubs for external library %s: %w", key, err) } } return nil @@ -459,29 +456,44 @@ func (e *Engine) mustSchema(ctx context.Context, name string, built map[string]* // Build a module and publish its schema. // // Assumes that all dependencies have been built and are available in "built". -func (e *Engine) build(ctx context.Context, name string, built map[string]*schema.Module, schemas chan<- *schema.Module) error { +func (e *Engine) build(ctx context.Context, key string, built map[string]*schema.Module, schemas chan<- *schema.Module) error { + module, ok := e.modules[key] + if !ok { + return fmt.Errorf("module %q not found", key) + } + moduleConfig, ok := module.ModuleConfig() + if !ok { + return fmt.Errorf("can not build module %q as it is not a FTL module", key) + } + combined := map[string]*schema.Module{} - e.gatherSchemas(built, e.modules[name], combined) + if err := e.gatherSchemas(built, module, combined); err != nil { + return err + } sch := &schema.Schema{Modules: maps.Values(combined)} - module := e.modules[name] + err := Build(ctx, sch, module) if err != nil { return err } - moduleSchema, err := schema.ModuleFromProtoFile(filepath.Join(module.Dir, module.DeployDir, module.Schema)) + moduleSchema, err := schema.ModuleFromProtoFile(filepath.Join(moduleConfig.Dir, moduleConfig.DeployDir, moduleConfig.Schema)) if err != nil { - return fmt.Errorf("load schema %s: %w", name, err) + return fmt.Errorf("load schema %s: %w", key, err) } schemas <- moduleSchema return nil } -func (e *Engine) generateStubsForExternalLib(ctx context.Context, dir string, built map[string]*schema.Module) error { +func (e *Engine) generateStubsForExternalLib(ctx context.Context, key string, built map[string]*schema.Module) error { //TODO: support kotlin - lib, ok := e.modules[dir] + module, ok := e.modules[key] + if !ok { + return fmt.Errorf("library %q not found", key) + } + lib, ok := module.ExternalLibrary() if !ok { - return fmt.Errorf("external library %q not found", dir) + return fmt.Errorf("module %q is not a library", key) } logger := log.FromContext(ctx) @@ -496,9 +508,10 @@ func (e *Engine) generateStubsForExternalLib(ctx context.Context, dir string, bu ftlVersion = ftl.Version } - //TODO: does gather schemas need to be able to check for missing dependencies? combined := map[string]*schema.Module{} - e.gatherSchemas(built, lib, combined) + if err := e.gatherSchemas(built, module, combined); err != nil { + return err + } sch := &schema.Schema{Modules: maps.Values(combined)} err = compile.GenerateExternalModules(compile.ExternalModuleContext{ @@ -511,7 +524,7 @@ func (e *Engine) generateStubsForExternalLib(ctx context.Context, dir string, bu if err != nil { return fmt.Errorf("failed to generate external modules: %w", err) } - logger.Infof("Generated stubs %v for %s", lib.Dependencies, lib.Dir) + logger.Infof("Generated stubs %v for %s", module.Dependencies, lib.Dir) return nil } @@ -520,15 +533,20 @@ func (e *Engine) gatherSchemas( moduleSchemas map[string]*schema.Module, module Module, out map[string]*schema.Module, -) { - var latestModule Module - if module.ModuleConfig.Type == moduleconfig.FTL { - latestModule = e.modules[module.Module] - } else { +) error { + latestModule, ok := e.modules[module.Key()] + if !ok { latestModule = module } for _, dep := range latestModule.Dependencies { out[dep] = moduleSchemas[dep] - e.gatherSchemas(moduleSchemas, e.modules[dep], out) + if dep != "builtin" { + depModule, ok := e.modules[dep] + if !ok { + return fmt.Errorf("dependency %q not found", dep) + } + e.gatherSchemas(moduleSchemas, depModule, out) + } } + return nil } diff --git a/buildengine/filehash.go b/buildengine/filehash.go index 8c3374ff98..7bf4ec09b7 100644 --- a/buildengine/filehash.go +++ b/buildengine/filehash.go @@ -9,8 +9,6 @@ import ( "path/filepath" "github.com/bmatcuk/doublestar/v4" - - "github.com/TBD54566975/ftl/common/moduleconfig" ) type FileChangeType rune @@ -65,11 +63,11 @@ func CompareFileHashes(oldFiles, newFiles FileHashes) (FileChangeType, string, b // ComputeFileHashes computes the SHA256 hash of all (non-git-ignored) files in // the given directory. -func ComputeFileHashes(config moduleconfig.ModuleConfig) (FileHashes, error) { +func ComputeFileHashes(module Module) (FileHashes, error) { fileHashes := make(FileHashes) - err := WalkDir(config.Dir, func(srcPath string, entry fs.DirEntry) error { - for _, pattern := range config.Watch { - relativePath, err := filepath.Rel(config.Dir, srcPath) + err := WalkDir(module.Dir(), func(srcPath string, entry fs.DirEntry) error { + for _, pattern := range module.Watch() { + relativePath, err := filepath.Rel(module.Dir(), srcPath) if err != nil { return err } diff --git a/buildengine/watch.go b/buildengine/watch.go index a567f8449b..a00aeaa276 100644 --- a/buildengine/watch.go +++ b/buildengine/watch.go @@ -6,7 +6,6 @@ import ( "github.com/alecthomas/types/pubsub" - "github.com/TBD54566975/ftl/common/moduleconfig" "github.com/TBD54566975/ftl/internal/log" "github.com/TBD54566975/ftl/internal/maps" ) @@ -56,35 +55,39 @@ func Watch(ctx context.Context, period time.Duration, dirs []string, externalLib } // Find all modules in the given directories. - moduleConfigs, err := DiscoverModules(ctx, dirs...) + modules, err := DiscoverModules(ctx, dirs...) if err != nil { logger.Tracef("error discovering modules: %v", err) continue } for _, dir := range externalLibDirs { - if moduleConfig, err := moduleconfig.LoadExternalLibraryConfig(dir); err == nil { - moduleConfigs = append(moduleConfigs, moduleConfig) + if module, err := LoadExternalLibrary(ctx, dir); err == nil { + modules = append(modules, module) } } - moduleConfigsByDir := maps.FromSlice(moduleConfigs, func(config moduleconfig.ModuleConfig) (string, moduleconfig.ModuleConfig) { - return config.Module, config + modulesByDir := maps.FromSlice(modules, func(module Module) (string, Module) { + return module.Dir(), module }) // Trigger events for removed modules. for _, existingModule := range existingModules { - if _, haveModule := moduleConfigsByDir[existingModule.Module.Module]; !haveModule { - logger.Debugf("module %s removed: %s", existingModule.Module.Module, existingModule.Module.Dir) + if _, haveModule := modulesByDir[existingModule.Module.Dir()]; !haveModule { + if _, ok := existingModule.Module.ModuleConfig(); ok { + logger.Debugf("%s %s removed: %s", existingModule.Module.Kind(), existingModule.Module.Key(), existingModule.Module.Dir()) + } else { + logger.Debugf("%s removed: %s", existingModule.Module.Kind(), existingModule.Module.Dir()) + } topic.Publish(WatchEventModuleRemoved{Module: existingModule.Module}) - delete(existingModules, existingModule.Module.ModuleConfig.Dir) + delete(existingModules, existingModule.Module.Dir()) } } // Compare the modules to the existing modules. - for _, config := range moduleConfigs { - existingModule, haveExistingModule := existingModules[config.Dir] - hashes, err := ComputeFileHashes(config) + for _, module := range modulesByDir { + existingModule, haveExistingModule := existingModules[module.Dir()] + hashes, err := ComputeFileHashes(module) if err != nil { - logger.Tracef("error computing file hashes for %s: %v", config.Dir, err) + logger.Tracef("error computing file hashes for %s: %v", module.Dir(), err) continue } @@ -93,19 +96,19 @@ func Watch(ctx context.Context, period time.Duration, dirs []string, externalLib if equal { continue } - logger.Debugf("module %s changed: %c%s", existingModule.Module.Module, changeType, path) + logger.Debugf("%s %s changed: %c%s", module.Kind(), module.Key(), changeType, path) topic.Publish(WatchEventModuleChanged{Module: existingModule.Module, Change: changeType, Path: path}) - existingModules[config.Dir] = moduleHashes{Hashes: hashes, Module: existingModule.Module} + existingModules[module.Dir()] = moduleHashes{Hashes: hashes, Module: existingModule.Module} continue } - - module, err := UpdateDependencies(ctx, config) - if err != nil { - continue + if _, ok := module.ModuleConfig(); ok { + logger.Debugf("%s %s added: %s", module.Kind(), module.Key(), module.Dir()) + } else { + logger.Debugf("%s added: %s", module.Kind(), module.Dir()) } - logger.Debugf("module %s added: %s", module.Module, module.Dir) + topic.Publish(WatchEventModuleAdded{Module: module}) - existingModules[config.Dir] = moduleHashes{Hashes: hashes, Module: module} + existingModules[module.Dir()] = moduleHashes{Hashes: hashes, Module: module} } } }() diff --git a/common/moduleconfig/moduleconfig.go b/common/moduleconfig/moduleconfig.go index 5b59a69c0d..20457e1abb 100644 --- a/common/moduleconfig/moduleconfig.go +++ b/common/moduleconfig/moduleconfig.go @@ -2,7 +2,6 @@ package moduleconfig import ( "fmt" - "os" "path/filepath" "strings" @@ -15,21 +14,12 @@ type ModuleGoConfig struct{} // ModuleKotlinConfig is language-specific configuration for Kotlin modules. type ModuleKotlinConfig struct{} -// ModuleType represents the type of the module. -type ModuleType int - -const ( - FTL ModuleType = iota - ExternalLibrary -) - // ModuleConfig is the configuration for an FTL module. // // Module config files are currently TOML. type ModuleConfig struct { // Dir is the root of the module. - Dir string `toml:"-"` - Type ModuleType `toml:"-"` + Dir string `toml:"-"` Language string `toml:"language"` Realm string `toml:"realm"` @@ -60,40 +50,10 @@ func LoadModuleConfig(dir string) (ModuleConfig, error) { if err := setConfigDefaults(dir, &config); err != nil { return config, fmt.Errorf("%s: %w", path, err) } - config.Type = FTL config.Dir = dir return config, nil } -func LoadExternalLibraryConfig(dir string) (ModuleConfig, error) { - config := ModuleConfig{ - Type: ExternalLibrary, - Dir: dir, - Module: dir, - } - - goModPath := filepath.Join(dir, "go.mod") - pomPath := filepath.Join(dir, "pom.xml") - if _, err := os.Stat(goModPath); err == nil { - config.Language = "go" - config.Watch = []string{"**/*.go", "go.mod", "go.sum"} - } else if !os.IsNotExist(err) { - return ModuleConfig{}, err - } else { - if _, err = os.Stat(pomPath); err == nil { - config.Language = "kotlin" - config.Watch = []string{"pom.xml", "src/**", "target/generated-sources"} - } else if !os.IsNotExist(err) { - return ModuleConfig{}, err - } - } - if config.Language == "" { - return ModuleConfig{}, fmt.Errorf("could not autodetect language: no go.mod or pom.xml found in %s", dir) - } - - return config, nil -} - // AbsDeployDir returns the absolute path to the deploy directory. func (c ModuleConfig) AbsDeployDir() string { return filepath.Join(c.Dir, c.DeployDir)