diff --git a/go-runtime/compile/build.go b/go-runtime/compile/build.go index 5968a0b9dc..631ad1c54f 100644 --- a/go-runtime/compile/build.go +++ b/go-runtime/compile/build.go @@ -2,6 +2,7 @@ package compile import ( "context" + "errors" "fmt" "os" "path" @@ -31,12 +32,17 @@ import ( "github.com/TBD54566975/ftl/internal/projectconfig" "github.com/TBD54566975/ftl/internal/reflect" "github.com/TBD54566975/ftl/internal/schema" + islices "github.com/TBD54566975/ftl/internal/slices" "github.com/TBD54566975/ftl/internal/watch" ) +var ErrInvalidateDependencies = errors.New("dependencies need to be updated") +var ftlTypesFilename = "types.ftl.go" + type MainWorkContext struct { GoVersion string SharedModulesPaths []string + IncludeMainPackage bool } type mainModuleContext struct { @@ -78,7 +84,9 @@ func (c *mainModuleContext) generateMainImports() []string { for _, e := range c.MainCtx.ExternalTypes { imports.Add(e.importStatement()) } - return imports.ToSlice() + out := imports.ToSlice() + slices.Sort(out) + return out } func (c *mainModuleContext) generateTypesImports(mainModuleImport string) []string { @@ -112,6 +120,7 @@ func (c *mainModuleContext) generateTypesImports(mainModuleImport string) []stri } filteredImports = append(filteredImports, im) } + slices.Sort(filteredImports) return filteredImports } @@ -262,8 +271,55 @@ func buildDir(moduleDir string) string { return filepath.Join(moduleDir, buildDirName) } +// OngoingState maintains state between builds, allowing the Build function to skip steps if nothing has changed. +type OngoingState struct { + imports []string + moduleCtx mainModuleContext +} + +func (s *OngoingState) checkIfImportsChanged(imports []string) (changed bool) { + if slices.Equal(s.imports, imports) { + return false + } + s.imports = imports + return true +} + +func (s *OngoingState) checkIfMainModuleContextChanged(moduleCtx mainModuleContext) (changed bool) { + if stdreflect.DeepEqual(s.moduleCtx, moduleCtx) { + return false + } + s.moduleCtx = moduleCtx + return true +} + +// DetectedFileChanges should be called whenever file changes are detected outside of the Build() function. +// This allows the OngoingState to detect if files need to be reprocessed. +func (s *OngoingState) DetectedFileChanges(config moduleconfig.AbsModuleConfig, changes []watch.FileChange) { + paths := []string{ + filepath.Join(config.Dir, ftlTypesFilename), + filepath.Join(config.Dir, "go.mod"), + filepath.Join(config.Dir, "go.sum"), + } + for _, change := range changes { + if !slices.Contains(paths, change.Path) { + continue + } + // If files altered by Build() have been manually changed, reset state to make sure we correct them if needed. + s.reset() + return + } +} + +func (s *OngoingState) reset() { + s.imports = nil + s.moduleCtx = mainModuleContext{} +} + // Build the given module. -func Build(ctx context.Context, projectRootDir, stubsRoot string, config moduleconfig.AbsModuleConfig, sch *schema.Schema, filesTransaction watch.ModifyFilesTransaction, buildEnv []string, devMode bool) (moduleSch optional.Option[*schema.Module], buildErrors []builderrors.Error, err error) { +func Build(ctx context.Context, projectRootDir, stubsRoot string, config moduleconfig.AbsModuleConfig, + sch *schema.Schema, deps, buildEnv []string, filesTransaction watch.ModifyFilesTransaction, ongoingState *OngoingState, + devMode bool) (moduleSch optional.Option[*schema.Module], buildErrors []builderrors.Error, err error) { if err := filesTransaction.Begin(); err != nil { return moduleSch, nil, fmt.Errorf("could not start a file transaction: %w", err) } @@ -271,8 +327,24 @@ func Build(ctx context.Context, projectRootDir, stubsRoot string, config modulec if terr := filesTransaction.End(); terr != nil { err = fmt.Errorf("failed to end file transaction: %w", terr) } + if err != nil { + // If we failed, reset the state to ensure we don't skip steps on the next build. + // Example: If `go mod tidy` fails due to a network failure, we need to try again next time, even if nothing else has changed. + ongoingState.reset() + } }() + // Check dependencies + newDeps, imports, err := extractDependenciesAndImports(config) + if err != nil { + return moduleSch, nil, fmt.Errorf("could not extract dependencies: %w", err) + } + importsChanged := ongoingState.checkIfImportsChanged(imports) + if !slices.Equal(islices.Sort(newDeps), islices.Sort(deps)) { + // dependencies have changed + return moduleSch, nil, ErrInvalidateDependencies + } + replacements, goModVersion, err := updateGoModule(filepath.Join(config.Dir, "go.mod")) if err != nil { return moduleSch, nil, err @@ -283,11 +355,6 @@ func Build(ctx context.Context, projectRootDir, stubsRoot string, config modulec return moduleSch, nil, fmt.Errorf("go version %q is not recent enough for this module, needs minimum version %q", goVersion, goModVersion) } - ftlVersion := "" - if ftl.IsRelease(ftl.Version) { - ftlVersion = ftl.Version - } - projectName := "" if pcpath, ok := projectconfig.DefaultConfigPath().Get(); ok { pc, err := projectconfig.Load(ctx, pcpath) @@ -317,6 +384,7 @@ func Build(ctx context.Context, projectRootDir, stubsRoot string, config modulec if err := internal.ScaffoldZip(mainWorkTemplateFiles(), config.Dir, MainWorkContext{ GoVersion: goModVersion, SharedModulesPaths: sharedModulesPaths, + IncludeMainPackage: mainPackageExists(config), }, scaffolder.Exclude("^go.mod$"), scaffolder.Functions(funcs)); err != nil { return moduleSch, nil, fmt.Errorf("failed to scaffold zip: %w", err) } @@ -334,25 +402,33 @@ func Build(ctx context.Context, projectRootDir, stubsRoot string, config modulec } logger.Debugf("Generating main module") - mctx, err := buildMainModuleContext(sch, result, goModVersion, ftlVersion, projectName, sharedModulesPaths, + mctx, err := buildMainModuleContext(sch, result, goModVersion, projectName, sharedModulesPaths, replacements) if err != nil { return moduleSch, nil, err } - if err := internal.ScaffoldZip(buildTemplateFiles(), config.Dir, mctx, scaffolder.Exclude("^go.mod$"), - scaffolder.Functions(funcs)); err != nil { - return moduleSch, nil, fmt.Errorf("failed to scaffold build template: %w", err) - } + mainModuleCtxChanged := ongoingState.checkIfMainModuleContextChanged(mctx) + if mainModuleCtxChanged { + if err := internal.ScaffoldZip(buildTemplateFiles(), config.Dir, mctx, scaffolder.Exclude("^go.mod$"), + scaffolder.Functions(funcs)); err != nil { + return moduleSch, nil, fmt.Errorf("failed to scaffold build template: %w", err) + } + if err := filesTransaction.ModifiedFiles(filepath.Join(config.Dir, ftlTypesFilename)); err != nil { + return moduleSch, nil, fmt.Errorf("failed to mark %s as modified: %w", ftlTypesFilename, err) + } + } logger.Debugf("Tidying go.mod files") wg, wgctx := errgroup.WithContext(ctx) - ftlTypesFilename := "types.ftl.go" wg.Go(func() error { + if !importsChanged { + log.FromContext(ctx).Debugf("skipped go mod tidy (module dir)\n") + return nil + } if err := exec.Command(wgctx, log.Debug, config.Dir, "go", "mod", "tidy").RunStderrError(wgctx); err != nil { return fmt.Errorf("%s: failed to tidy go.mod: %w", config.Dir, err) } - if err := exec.Command(wgctx, log.Debug, config.Dir, "go", "fmt", ftlTypesFilename).RunStderrError(wgctx); err != nil { return fmt.Errorf("%s: failed to format module dir: %w", config.Dir, err) } @@ -360,6 +436,10 @@ func Build(ctx context.Context, projectRootDir, stubsRoot string, config modulec }) mainDir := filepath.Join(buildDir, "go", "main") wg.Go(func() error { + if !mainModuleCtxChanged { + log.FromContext(ctx).Debugf("skipped go mod tidy (build dir)\n") + return nil + } if err := exec.Command(wgctx, log.Debug, mainDir, "go", "mod", "tidy").RunStderrError(wgctx); err != nil { return fmt.Errorf("%s: failed to tidy go.mod: %w", mainDir, err) } @@ -405,8 +485,12 @@ type mainModuleContextBuilder struct { imports map[string]string } -func buildMainModuleContext(sch *schema.Schema, result extract.Result, goModVersion, ftlVersion, projectName string, +func buildMainModuleContext(sch *schema.Schema, result extract.Result, goModVersion, projectName string, sharedModulesPaths []string, replacements []*modfile.Replace) (mainModuleContext, error) { + ftlVersion := "" + if ftl.IsRelease(ftl.Version) { + ftlVersion = ftl.Version + } combinedSch := &schema.Schema{ Modules: append(sch.Modules, result.Module), } diff --git a/go-runtime/compile/dependencies.go b/go-runtime/compile/dependencies.go index 6a11c10295..6232c74ede 100644 --- a/go-runtime/compile/dependencies.go +++ b/go-runtime/compile/dependencies.go @@ -16,9 +16,15 @@ import ( ) func ExtractDependencies(config moduleconfig.AbsModuleConfig) ([]string, error) { + deps, _, err := extractDependenciesAndImports(config) + return deps, err +} + +func extractDependenciesAndImports(config moduleconfig.AbsModuleConfig) (deps []string, imports []string, err error) { + importsMap := map[string]bool{} dependencies := map[string]bool{} fset := token.NewFileSet() - err := watch.WalkDir(config.Dir, func(path string, d fs.DirEntry) error { + err = watch.WalkDir(config.Dir, func(path string, d fs.DirEntry) error { if !d.IsDir() { return nil } @@ -36,6 +42,7 @@ func ExtractDependencies(config moduleconfig.AbsModuleConfig) ([]string, error) if err != nil { continue } + importsMap[path] = true if !strings.HasPrefix(path, "ftl/") { continue } @@ -50,9 +57,11 @@ func ExtractDependencies(config moduleconfig.AbsModuleConfig) ([]string, error) return nil }) if err != nil { - return nil, fmt.Errorf("%s: failed to extract dependencies from Go module: %w", config.Module, err) + return nil, nil, fmt.Errorf("%s: failed to extract dependencies from Go module: %w", config.Module, err) } modules := maps.Keys(dependencies) sort.Strings(modules) - return modules, nil + imports = maps.Keys(importsMap) + sort.Strings(imports) + return modules, imports, nil } diff --git a/go-runtime/compile/main-work-template/go.work.tmpl b/go-runtime/compile/main-work-template/go.work.tmpl index 1dbed607a3..7e417ed74d 100644 --- a/go-runtime/compile/main-work-template/go.work.tmpl +++ b/go-runtime/compile/main-work-template/go.work.tmpl @@ -5,4 +5,7 @@ use ( {{- range .SharedModulesPaths }} {{ . }} {{- end }} +{{ if .IncludeMainPackage }} + .ftl/go/main +{{ end }} ) diff --git a/go-runtime/compile/stubs.go b/go-runtime/compile/stubs.go index 618b78e8eb..794e9885a3 100644 --- a/go-runtime/compile/stubs.go +++ b/go-runtime/compile/stubs.go @@ -2,7 +2,9 @@ package compile import ( "context" + "errors" "fmt" + "os" "path/filepath" "runtime" @@ -102,8 +104,15 @@ func SyncGeneratedStubReferences(ctx context.Context, config moduleconfig.AbsMod if err := internal.ScaffoldZip(mainWorkTemplateFiles(), config.Dir, MainWorkContext{ GoVersion: goModVersion, SharedModulesPaths: sharedModulePaths, + IncludeMainPackage: mainPackageExists(config), }, scaffolder.Exclude("^go.mod$"), scaffolder.Functions(funcs)); err != nil { return fmt.Errorf("failed to scaffold zip: %w", err) } return nil } + +func mainPackageExists(config moduleconfig.AbsModuleConfig) bool { + // check if main package exists, otherwise do not include it + _, err := os.Stat(filepath.Join(buildDir(config.Dir), "go", "main", "go.mod")) + return !errors.Is(err, os.ErrNotExist) +} diff --git a/go-runtime/goplugin/service.go b/go-runtime/goplugin/service.go index 05cdcf198c..1738e3d05a 100644 --- a/go-runtime/goplugin/service.go +++ b/go-runtime/goplugin/service.go @@ -2,10 +2,10 @@ package goplugin import ( "context" + "errors" "fmt" "path/filepath" "runtime" - "slices" "strings" "time" @@ -28,7 +28,6 @@ import ( "github.com/TBD54566975/ftl/internal/log" "github.com/TBD54566975/ftl/internal/moduleconfig" "github.com/TBD54566975/ftl/internal/schema" - islices "github.com/TBD54566975/ftl/internal/slices" "github.com/TBD54566975/ftl/internal/watch" ) @@ -43,7 +42,9 @@ type buildContextUpdatedEvent struct { func (buildContextUpdatedEvent) updateEvent() {} -type filesUpdatedEvent struct{} +type filesUpdatedEvent struct { + changes []watch.FileChange +} func (filesUpdatedEvent) updateEvent() {} @@ -237,6 +238,7 @@ func (s *Service) Build(ctx context.Context, req *connect.Request[langpb.BuildRe } watcher := watch.NewWatcher(watchPatterns...) + if req.Msg.RebuildAutomatically { s.acceptsContextUpdates.Store(true) defer s.acceptsContextUpdates.Store(false) @@ -247,7 +249,8 @@ func (s *Service) Build(ctx context.Context, req *connect.Request[langpb.BuildRe } // Initial build - if err := buildAndSend(ctx, stream, req.Msg.ProjectRoot, req.Msg.StubsRoot, buildCtx, false, watcher.GetTransaction(buildCtx.Config.Dir)); err != nil { + ongoingState := &compile.OngoingState{} + if err := buildAndSend(ctx, stream, req.Msg.ProjectRoot, req.Msg.StubsRoot, buildCtx, false, watcher.GetTransaction(buildCtx.Config.Dir), ongoingState); err != nil { return err } if !req.Msg.RebuildAutomatically { @@ -259,7 +262,7 @@ func (s *Service) Build(ctx context.Context, req *connect.Request[langpb.BuildRe select { case e := <-events: var isAutomaticRebuild bool - buildCtx, isAutomaticRebuild = buildContextFromPendingEvents(ctx, buildCtx, events, e) + buildCtx, isAutomaticRebuild = buildContextFromPendingEvents(ctx, buildCtx, events, e, ongoingState) if isAutomaticRebuild { err = stream.Send(&langpb.BuildEvent{ Event: &langpb.BuildEvent_AutoRebuildStarted{ @@ -272,7 +275,7 @@ func (s *Service) Build(ctx context.Context, req *connect.Request[langpb.BuildRe return fmt.Errorf("could not send auto rebuild started event: %w", err) } } - if err = buildAndSend(ctx, stream, req.Msg.ProjectRoot, req.Msg.StubsRoot, buildCtx, isAutomaticRebuild, watcher.GetTransaction(buildCtx.Config.Dir)); err != nil { + if err = buildAndSend(ctx, stream, req.Msg.ProjectRoot, req.Msg.StubsRoot, buildCtx, isAutomaticRebuild, watcher.GetTransaction(buildCtx.Config.Dir), ongoingState); err != nil { return err } case <-ctx.Done(): @@ -294,11 +297,9 @@ func (s *Service) BuildContextUpdated(ctx context.Context, req *connect.Request[ if err != nil { return nil, err } - s.updatesTopic.Publish(buildContextUpdatedEvent{ buildCtx: buildCtx, }) - return connect.NewResponse(&langpb.BuildContextUpdatedResponse{}), nil } @@ -328,9 +329,9 @@ func watchFiles(ctx context.Context, watcher *watch.Watcher, buildCtx buildConte for { select { case e := <-watchEvents: - if _, ok := e.(watch.WatchEventModuleChanged); ok { - log.FromContext(ctx).Infof("Found file changes: %s", buildCtx.Config.Dir) - events <- filesUpdatedEvent{} + if change, ok := e.(watch.WatchEventModuleChanged); ok { + log.FromContext(ctx).Infof("Found file changes: %s", change) + events <- filesUpdatedEvent{changes: change.Changes} } case <-ctx.Done(): @@ -342,7 +343,7 @@ func watchFiles(ctx context.Context, watcher *watch.Watcher, buildCtx buildConte } // buildContextFromPendingEvents processes all pending events to determine the latest context and whether the build is automatic. -func buildContextFromPendingEvents(ctx context.Context, buildCtx buildContext, events chan updateEvent, firstEvent updateEvent) (newBuildCtx buildContext, isAutomaticRebuild bool) { +func buildContextFromPendingEvents(ctx context.Context, buildCtx buildContext, events chan updateEvent, firstEvent updateEvent, ongoingState *compile.OngoingState) (newBuildCtx buildContext, isAutomaticRebuild bool) { allEvents := []updateEvent{firstEvent} // find any other events in the queue for { @@ -360,14 +361,15 @@ func buildContextFromPendingEvents(ctx context.Context, buildCtx buildContext, e buildCtx = e.buildCtx hasExplicitBuilt = true case filesUpdatedEvent: + ongoingState.DetectedFileChanges(buildCtx.Config, e.changes) } - } switch e := firstEvent.(type) { case buildContextUpdatedEvent: buildCtx = e.buildCtx hasExplicitBuilt = true case filesUpdatedEvent: + ongoingState.DetectedFileChanges(buildCtx.Config, e.changes) } return buildCtx, !hasExplicitBuilt } @@ -378,8 +380,9 @@ func buildContextFromPendingEvents(ctx context.Context, buildCtx buildContext, e // // Build errors are sent over the stream as a BuildFailure event. // This function only returns an error if events could not be send over the stream. -func buildAndSend(ctx context.Context, stream *connect.ServerStream[langpb.BuildEvent], projectRoot, stubsRoot string, buildCtx buildContext, isAutomaticRebuild bool, transaction watch.ModifyFilesTransaction) error { - buildEvent, err := build(ctx, projectRoot, stubsRoot, buildCtx, isAutomaticRebuild, transaction) +func buildAndSend(ctx context.Context, stream *connect.ServerStream[langpb.BuildEvent], projectRoot, stubsRoot string, buildCtx buildContext, + isAutomaticRebuild bool, transaction watch.ModifyFilesTransaction, ongoingState *compile.OngoingState) error { + buildEvent, err := build(ctx, projectRoot, stubsRoot, buildCtx, isAutomaticRebuild, transaction, ongoingState) if err != nil { buildEvent = buildFailure(buildCtx, isAutomaticRebuild, builderrors.Error{ Type: builderrors.FTL, @@ -393,33 +396,27 @@ func buildAndSend(ctx context.Context, stream *connect.ServerStream[langpb.Build return nil } -func build(ctx context.Context, projectRoot, stubsRoot string, buildCtx buildContext, isAutomaticRebuild bool, transaction watch.ModifyFilesTransaction) (*langpb.BuildEvent, error) { +func build(ctx context.Context, projectRoot, stubsRoot string, buildCtx buildContext, isAutomaticRebuild bool, transaction watch.ModifyFilesTransaction, + ongoingState *compile.OngoingState) (*langpb.BuildEvent, error) { release, err := flock.Acquire(ctx, buildCtx.Config.BuildLock, BuildLockTimeout) if err != nil { return nil, fmt.Errorf("could not acquire build lock: %w", err) } defer release() //nolint:errcheck - deps, err := compile.ExtractDependencies(buildCtx.Config) + m, buildErrs, err := compile.Build(ctx, projectRoot, stubsRoot, buildCtx.Config, buildCtx.Schema, buildCtx.Dependencies, buildCtx.BuildEnv, transaction, ongoingState, false) if err != nil { - return nil, fmt.Errorf("could not extract dependencies: %w", err) - } - - if !slices.Equal(islices.Sort(deps), islices.Sort(buildCtx.Dependencies)) { - // dependencies have changed - return &langpb.BuildEvent{ - Event: &langpb.BuildEvent_BuildFailure{ - BuildFailure: &langpb.BuildFailure{ - ContextId: buildCtx.ID, - IsAutomaticRebuild: isAutomaticRebuild, - InvalidateDependencies: true, + if errors.Is(err, compile.ErrInvalidateDependencies) { + return &langpb.BuildEvent{ + Event: &langpb.BuildEvent_BuildFailure{ + BuildFailure: &langpb.BuildFailure{ + ContextId: buildCtx.ID, + IsAutomaticRebuild: isAutomaticRebuild, + InvalidateDependencies: true, + }, }, - }, - }, nil - } - - m, buildErrs, err := compile.Build(ctx, projectRoot, stubsRoot, buildCtx.Config, buildCtx.Schema, transaction, buildCtx.BuildEnv, false) - if err != nil { + }, nil + } return buildFailure(buildCtx, isAutomaticRebuild, builderrors.Error{ Type: builderrors.COMPILER, Level: builderrors.ERROR, diff --git a/internal/watch/filehash.go b/internal/watch/filehash.go index b61cb57692..dd52b4ca82 100644 --- a/internal/watch/filehash.go +++ b/internal/watch/filehash.go @@ -38,28 +38,26 @@ type FileHashes map[string][]byte // CompareFileHashes compares the hashes of the files in the oldFiles and newFiles maps. // -// Returns true if the hashes are equal, false otherwise. -// -// If false, the returned string will be a file that caused the difference and the -// returned FileChangeType will be the type of change that occurred. -func CompareFileHashes(oldFiles, newFiles FileHashes) (FileChangeType, string, bool) { +// Returns all file changes +func CompareFileHashes(oldFiles, newFiles FileHashes) []FileChange { + changes := []FileChange{} for key, hash1 := range oldFiles { hash2, exists := newFiles[key] if !exists { - return FileRemoved, key, false + changes = append(changes, FileChange{Change: FileRemoved, Path: key}) } if !bytes.Equal(hash1, hash2) { - return FileChanged, key, false + changes = append(changes, FileChange{Change: FileChanged, Path: key}) } } for key := range newFiles { if _, exists := oldFiles[key]; !exists { - return FileAdded, key, false + changes = append(changes, FileChange{Change: FileAdded, Path: key}) } } - return ' ', "", true + return changes } // ComputeFileHashes computes the SHA256 hash of all (non-git-ignored) files in diff --git a/internal/watch/watch.go b/internal/watch/watch.go index f1d02b98db..8ee2835bfe 100644 --- a/internal/watch/watch.go +++ b/internal/watch/watch.go @@ -3,6 +3,8 @@ package watch import ( "context" "fmt" + "path/filepath" + "strings" "sync" "time" @@ -11,6 +13,7 @@ import ( "github.com/TBD54566975/ftl/internal/log" "github.com/TBD54566975/ftl/internal/maps" "github.com/TBD54566975/ftl/internal/moduleconfig" + "github.com/TBD54566975/ftl/internal/slices" ) // A WatchEvent is an event that occurs when a module is added, removed, or @@ -30,10 +33,24 @@ type WatchEventModuleRemoved struct { func (WatchEventModuleRemoved) watchEvent() {} type WatchEventModuleChanged struct { - Config moduleconfig.UnvalidatedModuleConfig + Config moduleconfig.UnvalidatedModuleConfig + Changes []FileChange + Time time.Time +} + +func (c WatchEventModuleChanged) String() string { + return strings.Join(slices.Map(c.Changes, func(change FileChange) string { + p, err := filepath.Rel(c.Config.Dir, change.Path) + if err != nil { + p = change.Path + } + return fmt.Sprintf("%s%s", change.Change, p) + }), ", ") +} + +type FileChange struct { Change FileChangeType Path string - Time time.Time } func (WatchEventModuleChanged) watchEvent() {} @@ -147,12 +164,13 @@ func (w *Watcher) Watch(ctx context.Context, period time.Duration, moduleDirs [] } if haveExistingModule { - changeType, path, equal := CompareFileHashes(existingModule.Hashes, hashes) - if equal { + changes := CompareFileHashes(existingModule.Hashes, hashes) + if len(changes) == 0 { continue } - logger.Debugf("changed %q: %c%s", config.Module, changeType, path) - topic.Publish(WatchEventModuleChanged{Config: existingModule.Config, Change: changeType, Path: path, Time: time.Now()}) + event := WatchEventModuleChanged{Config: existingModule.Config, Changes: changes, Time: time.Now()} + logger.Debugf("changed %q: %s", config.Module, event) + topic.Publish(event) w.existingModules[config.Dir] = moduleHashes{Hashes: hashes, Config: existingModule.Config} continue } diff --git a/jvm-runtime/plugin/common/jvmcommon.go b/jvm-runtime/plugin/common/jvmcommon.go index e80d78618c..e1900e135a 100644 --- a/jvm-runtime/plugin/common/jvmcommon.go +++ b/jvm-runtime/plugin/common/jvmcommon.go @@ -366,7 +366,7 @@ func watchFiles(ctx context.Context, watcher *watch.Watcher, buildCtx buildConte select { case e := <-watchEvents: if change, ok := e.(watch.WatchEventModuleChanged); ok { - log.FromContext(ctx).Infof("Found file changes: %s", change.Path) + log.FromContext(ctx).Infof("Found file changes: %s", change) events <- filesUpdatedEvent{} }