diff --git a/internal/fingerprint/checker.go b/internal/fingerprint/checker.go index d630c18b86..6b81a23e69 100644 --- a/internal/fingerprint/checker.go +++ b/internal/fingerprint/checker.go @@ -13,6 +13,7 @@ type StatusCheckable interface { // SourcesCheckable defines any type that can check if the sources of a task are up-to-date. type SourcesCheckable interface { + SetUpToDate(t *ast.Task) error IsUpToDate(t *ast.Task) (bool, error) Value(t *ast.Task) (any, error) OnError(t *ast.Task) error diff --git a/internal/fingerprint/sources_checksum.go b/internal/fingerprint/sources_checksum.go index 91cb6e384a..9cccd1f795 100644 --- a/internal/fingerprint/sources_checksum.go +++ b/internal/fingerprint/sources_checksum.go @@ -29,51 +29,80 @@ func NewChecksumChecker(tempDir string, dry bool) *ChecksumChecker { } func (checker *ChecksumChecker) IsUpToDate(t *ast.Task) (bool, error) { - if len(t.Sources) == 0 { + if len(t.Sources) == 0 && len(t.Generates) == 0 { return false, nil } checksumFile := checker.checksumFilePath(t) data, _ := os.ReadFile(checksumFile) - oldHash := strings.TrimSpace(string(data)) + oldHashes := strings.TrimSpace(string(data)) + oldSourcesHash, oldGeneratesdHash, _ := strings.Cut(oldHashes, "\n") - newHash, err := checker.checksum(t) + newSourcesHash, err := checker.checksum(t, t.Sources) if err != nil { - return false, nil + return false, err } - if !checker.dry && oldHash != newHash { + newGeneratesHash, err := checker.checksum(t, t.Generates) + if err != nil { + return false, err + } + + if !checker.dry && oldSourcesHash != newSourcesHash { + // make sure current sources hash are saved to file before executing the task, + // the proper "generated" hash will be saved after the task is executed _ = os.MkdirAll(filepathext.SmartJoin(checker.tempDir, "checksum"), 0o755) - if err = os.WriteFile(checksumFile, []byte(newHash+"\n"), 0o644); err != nil { + if err = os.WriteFile(checksumFile, []byte(newSourcesHash+"\n"+"_"), 0o644); err != nil { return false, err } } - if len(t.Generates) > 0 { - // For each specified 'generates' field, check whether the files actually exist - for _, g := range t.Generates { - if g.Negate { - continue - } - generates, err := Glob(t.Dir, g.Glob) - if os.IsNotExist(err) { - return false, nil - } - if err != nil { - return false, err - } - if len(generates) == 0 { - return false, nil - } + return oldSourcesHash == newSourcesHash && oldGeneratesdHash == newGeneratesHash, nil +} + +func (checker *ChecksumChecker) SetUpToDate(t *ast.Task) error { + if len(t.Sources) == 0 && len(t.Generates) == 0 { + return nil + } + + if checker.dry { + return nil + } + + checksumFile := checker.checksumFilePath(t) + + data, _ := os.ReadFile(checksumFile) + oldHashes := strings.TrimSpace(string(data)) + oldSourcesHash, oldGeneratesdHash, _ := strings.Cut(oldHashes, "\n") + + newSourcesHash, err := checker.checksum(t, t.Sources) + if err != nil { + return err + } + + newGeneratesHash, err := checker.checksum(t, t.Generates) + if err != nil { + return err + } + + if oldSourcesHash != newSourcesHash || oldGeneratesdHash != newGeneratesHash { + _ = os.MkdirAll(filepathext.SmartJoin(checker.tempDir, "checksum"), 0o755) + if err = os.WriteFile(checksumFile, []byte(oldSourcesHash+"\n"+newGeneratesHash+"\n"), 0o644); err != nil { + return err } } - return oldHash == newHash, nil + return nil } func (checker *ChecksumChecker) Value(t *ast.Task) (any, error) { - return checker.checksum(t) + c1, err := checker.checksum(t, t.Sources) + if err != nil { + return c1, err + } + c2, err := checker.checksum(t, t.Generates) + return c1 + "\n" + c2, err } func (checker *ChecksumChecker) OnError(t *ast.Task) error { @@ -87,8 +116,8 @@ func (*ChecksumChecker) Kind() string { return "checksum" } -func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) { - sources, err := Globs(t.Dir, t.Sources) +func (c *ChecksumChecker) checksum(t *ast.Task, globs []*ast.Glob) (string, error) { + sources, err := Globs(t.Dir, globs) if err != nil { return "", err } diff --git a/internal/fingerprint/sources_none.go b/internal/fingerprint/sources_none.go index d13adc1c51..5832320715 100644 --- a/internal/fingerprint/sources_none.go +++ b/internal/fingerprint/sources_none.go @@ -10,6 +10,10 @@ func (NoneChecker) IsUpToDate(t *ast.Task) (bool, error) { return false, nil } +func (NoneChecker) SetUpToDate(t *ast.Task) error { + return nil +} + func (NoneChecker) Value(t *ast.Task) (any, error) { return "", nil } diff --git a/internal/fingerprint/sources_timestamp.go b/internal/fingerprint/sources_timestamp.go index b1a6f299d5..6ee0bb01e5 100644 --- a/internal/fingerprint/sources_timestamp.go +++ b/internal/fingerprint/sources_timestamp.go @@ -84,6 +84,10 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) { return !shouldUpdate, nil } +func (checker *TimestampChecker) SetUpToDate(t *ast.Task) error { + return nil // TODO: implement +} + func (checker *TimestampChecker) Kind() string { return "timestamp" } diff --git a/internal/fingerprint/task.go b/internal/fingerprint/task.go index 2b48e114c9..cc15e2e5a0 100644 --- a/internal/fingerprint/task.go +++ b/internal/fingerprint/task.go @@ -93,7 +93,7 @@ func IsTaskUpToDate( } statusIsSet := len(t.Status) != 0 - sourcesIsSet := len(t.Sources) != 0 + sourcesIsSet := len(t.Sources) != 0 || len(t.Generates) != 0 // If status is set, check if it is up-to-date if statusIsSet { @@ -130,3 +130,41 @@ func IsTaskUpToDate( // i.e. it is never considered "up-to-date" return false, nil } + +func SetTaskUpToDate( + ctx context.Context, + t *ast.Task, + opts ...CheckerOption, +) error { + var err error + + // Default config + config := &CheckerConfig{ + method: "none", + tempDir: "", + dry: false, + logger: nil, + statusChecker: nil, + sourcesChecker: nil, + } + + // Apply functional options + for _, opt := range opts { + opt(config) + } + + // If no status checker was given, set up the default one + if config.statusChecker == nil { + config.statusChecker = NewStatusChecker(config.logger) + } + + // If no sources checker was given, set up the default one + if config.sourcesChecker == nil { + config.sourcesChecker, err = NewSourcesChecker(config.method, config.tempDir, config.dry) + if err != nil { + return err + } + } + + return config.sourcesChecker.SetUpToDate(t) +} diff --git a/internal/mocks/sources_checkable.go b/internal/mocks/sources_checkable.go index 1e43ce937a..b5048ab8b6 100644 --- a/internal/mocks/sources_checkable.go +++ b/internal/mocks/sources_checkable.go @@ -122,6 +122,52 @@ func (_c *SourcesCheckable_Kind_Call) RunAndReturn(run func() string) *SourcesCh return _c } +// SetUpToDate provides a mock function with given fields: t +func (_m *SourcesCheckable) SetUpToDate(t *ast.Task) error { + ret := _m.Called(t) + + if len(ret) == 0 { + panic("no return value specified for SetUpToDate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*ast.Task) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SourcesCheckable_SetUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetUpToDate' +type SourcesCheckable_SetUpToDate_Call struct { + *mock.Call +} + +// SetUpToDate is a helper method to define mock.On call +// - t *ast.Task +func (_e *SourcesCheckable_Expecter) SetUpToDate(t interface{}) *SourcesCheckable_SetUpToDate_Call { + return &SourcesCheckable_SetUpToDate_Call{Call: _e.mock.On("SetUpToDate", t)} +} + +func (_c *SourcesCheckable_SetUpToDate_Call) Run(run func(t *ast.Task)) *SourcesCheckable_SetUpToDate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*ast.Task)) + }) + return _c +} + +func (_c *SourcesCheckable_SetUpToDate_Call) Return(_a0 error) *SourcesCheckable_SetUpToDate_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SourcesCheckable_SetUpToDate_Call) RunAndReturn(run func(*ast.Task) error) *SourcesCheckable_SetUpToDate_Call { + _c.Call.Return(run) + return _c +} + // OnError provides a mock function with given fields: t func (_m *SourcesCheckable) OnError(t *ast.Task) error { ret := _m.Called(t) diff --git a/task.go b/task.go index 5c63de346b..3ca11eddb0 100644 --- a/task.go +++ b/task.go @@ -278,6 +278,20 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error { return &errors.TaskRunError{TaskName: t.Task, Err: err} } } + if !skipFingerprinting { + // Get the fingerprinting method to use + method := e.Taskfile.Method + if t.Method != "" { + method = t.Method + } + e.Logger.VerboseErrf(logger.Magenta, "task: %q setting task up to date\n", call.Task) + fingerprint.SetTaskUpToDate(ctx, t, + fingerprint.WithMethod(method), + fingerprint.WithTempDir(e.TempDir.Fingerprint), + fingerprint.WithDry(e.Dry), + fingerprint.WithLogger(e.Logger), + ) + } e.Logger.VerboseErrf(logger.Magenta, "task: %q finished\n", call.Task) return nil })