task.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. package task
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "os"
  7. "runtime"
  8. "slices"
  9. "sync"
  10. "sync/atomic"
  11. "time"
  12. "mvdan.cc/sh/v3/interp"
  13. "github.com/go-task/task/v3/errors"
  14. "github.com/go-task/task/v3/internal/compiler"
  15. "github.com/go-task/task/v3/internal/env"
  16. "github.com/go-task/task/v3/internal/execext"
  17. "github.com/go-task/task/v3/internal/fingerprint"
  18. "github.com/go-task/task/v3/internal/logger"
  19. "github.com/go-task/task/v3/internal/output"
  20. "github.com/go-task/task/v3/internal/slicesext"
  21. "github.com/go-task/task/v3/internal/sort"
  22. "github.com/go-task/task/v3/internal/summary"
  23. "github.com/go-task/task/v3/internal/templater"
  24. "github.com/go-task/task/v3/taskfile/ast"
  25. "github.com/sajari/fuzzy"
  26. "golang.org/x/sync/errgroup"
  27. )
  28. const (
  29. // MaximumTaskCall is the max number of times a task can be called.
  30. // This exists to prevent infinite loops on cyclic dependencies
  31. MaximumTaskCall = 1000
  32. )
  33. type TempDir struct {
  34. Remote string
  35. Fingerprint string
  36. }
  37. // Executor executes a Taskfile
  38. type Executor struct {
  39. Taskfile *ast.Taskfile
  40. Dir string
  41. Entrypoint string
  42. TempDir TempDir
  43. Force bool
  44. ForceAll bool
  45. Insecure bool
  46. Download bool
  47. Offline bool
  48. Timeout time.Duration
  49. Watch bool
  50. Verbose bool
  51. Silent bool
  52. AssumeYes bool
  53. AssumeTerm bool // Used for testing
  54. Dry bool
  55. Summary bool
  56. Parallel bool
  57. Color bool
  58. Concurrency int
  59. Interval time.Duration
  60. Stdin io.Reader
  61. Stdout io.Writer
  62. Stderr io.Writer
  63. Logger *logger.Logger
  64. Compiler *compiler.Compiler
  65. Output output.Output
  66. OutputStyle ast.Output
  67. TaskSorter sort.TaskSorter
  68. UserWorkingDir string
  69. fuzzyModel *fuzzy.Model
  70. concurrencySemaphore chan struct{}
  71. taskCallCount map[string]*int32
  72. mkdirMutexMap map[string]*sync.Mutex
  73. executionHashes map[string]context.Context
  74. executionHashesMutex sync.Mutex
  75. }
  76. // Run runs Task
  77. func (e *Executor) Run(ctx context.Context, calls ...*ast.Call) error {
  78. // check if given tasks exist
  79. for _, call := range calls {
  80. task, err := e.GetTask(call)
  81. if err != nil {
  82. if _, ok := err.(*errors.TaskNotFoundError); ok {
  83. if _, err := e.ListTasks(ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
  84. return err
  85. }
  86. }
  87. return err
  88. }
  89. if task.Internal {
  90. if _, ok := err.(*errors.TaskNotFoundError); ok {
  91. if _, err := e.ListTasks(ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
  92. return err
  93. }
  94. }
  95. return &errors.TaskInternalError{TaskName: call.Task}
  96. }
  97. }
  98. if e.Summary {
  99. for i, c := range calls {
  100. compiledTask, err := e.FastCompiledTask(c)
  101. if err != nil {
  102. return nil
  103. }
  104. summary.PrintSpaceBetweenSummaries(e.Logger, i)
  105. summary.PrintTask(e.Logger, compiledTask)
  106. }
  107. return nil
  108. }
  109. regularCalls, watchCalls, err := e.splitRegularAndWatchCalls(calls...)
  110. if err != nil {
  111. return err
  112. }
  113. g, ctx := errgroup.WithContext(ctx)
  114. for _, c := range regularCalls {
  115. c := c
  116. if e.Parallel {
  117. g.Go(func() error { return e.RunTask(ctx, c) })
  118. } else {
  119. if err := e.RunTask(ctx, c); err != nil {
  120. return err
  121. }
  122. }
  123. }
  124. if err := g.Wait(); err != nil {
  125. return err
  126. }
  127. if len(watchCalls) > 0 {
  128. return e.watchTasks(watchCalls...)
  129. }
  130. return nil
  131. }
  132. func (e *Executor) splitRegularAndWatchCalls(calls ...*ast.Call) (regularCalls []*ast.Call, watchCalls []*ast.Call, err error) {
  133. for _, c := range calls {
  134. t, err := e.GetTask(c)
  135. if err != nil {
  136. return nil, nil, err
  137. }
  138. if e.Watch || t.Watch {
  139. watchCalls = append(watchCalls, c)
  140. } else {
  141. regularCalls = append(regularCalls, c)
  142. }
  143. }
  144. return
  145. }
  146. // RunTask runs a task by its name
  147. func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
  148. t, err := e.FastCompiledTask(call)
  149. if err != nil {
  150. return err
  151. }
  152. if !shouldRunOnCurrentPlatform(t.Platforms) {
  153. e.Logger.VerboseOutf(logger.Yellow, `task: %q not for current platform - ignored\n`, call.Task)
  154. return nil
  155. }
  156. t, err = e.CompiledTask(call)
  157. if err != nil {
  158. return err
  159. }
  160. if !e.Watch && atomic.AddInt32(e.taskCallCount[t.Task], 1) >= MaximumTaskCall {
  161. return &errors.TaskCalledTooManyTimesError{
  162. TaskName: t.Task,
  163. MaximumTaskCall: MaximumTaskCall,
  164. }
  165. }
  166. release := e.acquireConcurrencyLimit()
  167. defer release()
  168. return e.startExecution(ctx, t, func(ctx context.Context) error {
  169. e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task)
  170. if err := e.runDeps(ctx, t); err != nil {
  171. return err
  172. }
  173. skipFingerprinting := e.ForceAll || (!call.Indirect && e.Force)
  174. if !skipFingerprinting {
  175. if err := ctx.Err(); err != nil {
  176. return err
  177. }
  178. if err := e.areTaskRequiredVarsSet(t, call); err != nil {
  179. return err
  180. }
  181. preCondMet, err := e.areTaskPreconditionsMet(ctx, t)
  182. if err != nil {
  183. return err
  184. }
  185. // Get the fingerprinting method to use
  186. method := e.Taskfile.Method
  187. if t.Method != "" {
  188. method = t.Method
  189. }
  190. upToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
  191. fingerprint.WithMethod(method),
  192. fingerprint.WithTempDir(e.TempDir.Fingerprint),
  193. fingerprint.WithDry(e.Dry),
  194. fingerprint.WithLogger(e.Logger),
  195. )
  196. if err != nil {
  197. return err
  198. }
  199. if upToDate && preCondMet {
  200. if e.Verbose || (!call.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
  201. e.Logger.Errf(logger.Magenta, "task: Task %q is up to date\n", t.Name())
  202. }
  203. return nil
  204. }
  205. }
  206. for _, p := range t.Prompt {
  207. if p != "" && !e.Dry {
  208. if err := e.Logger.Prompt(logger.Yellow, p, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
  209. return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
  210. } else if errors.Is(err, logger.ErrPromptCancelled) {
  211. return &errors.TaskCancelledByUserError{TaskName: call.Task}
  212. } else if err != nil {
  213. return err
  214. }
  215. }
  216. }
  217. if err := e.mkdir(t); err != nil {
  218. e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err)
  219. }
  220. var deferredExitCode uint8
  221. for i := range t.Cmds {
  222. if t.Cmds[i].Defer {
  223. defer e.runDeferred(t, call, i, &deferredExitCode)
  224. continue
  225. }
  226. if err := e.runCommand(ctx, t, call, i); err != nil {
  227. if err2 := e.statusOnError(t); err2 != nil {
  228. e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2)
  229. }
  230. exitCode, isExitError := interp.IsExitStatus(err)
  231. if isExitError {
  232. if t.IgnoreError {
  233. e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
  234. continue
  235. }
  236. deferredExitCode = exitCode
  237. }
  238. if call.Indirect {
  239. return err
  240. }
  241. return &errors.TaskRunError{TaskName: t.Task, Err: err}
  242. }
  243. }
  244. e.Logger.VerboseErrf(logger.Magenta, "task: %q finished\n", call.Task)
  245. return nil
  246. })
  247. }
  248. func (e *Executor) mkdir(t *ast.Task) error {
  249. if t.Dir == "" {
  250. return nil
  251. }
  252. mutex := e.mkdirMutexMap[t.Task]
  253. mutex.Lock()
  254. defer mutex.Unlock()
  255. if _, err := os.Stat(t.Dir); os.IsNotExist(err) {
  256. if err := os.MkdirAll(t.Dir, 0o755); err != nil {
  257. return err
  258. }
  259. }
  260. return nil
  261. }
  262. func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
  263. g, ctx := errgroup.WithContext(ctx)
  264. reacquire := e.releaseConcurrencyLimit()
  265. defer reacquire()
  266. for _, d := range t.Deps {
  267. d := d
  268. g.Go(func() error {
  269. err := e.RunTask(ctx, &ast.Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
  270. if err != nil {
  271. return err
  272. }
  273. return nil
  274. })
  275. }
  276. return g.Wait()
  277. }
  278. func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitCode *uint8) {
  279. ctx, cancel := context.WithCancel(context.Background())
  280. defer cancel()
  281. origTask, err := e.GetTask(call)
  282. if err != nil {
  283. return
  284. }
  285. cmd := t.Cmds[i]
  286. vars, _ := e.Compiler.GetVariables(origTask, call)
  287. cache := &templater.Cache{Vars: vars}
  288. extra := map[string]any{}
  289. if deferredExitCode != nil && *deferredExitCode > 0 {
  290. extra["EXIT_CODE"] = fmt.Sprintf("%d", *deferredExitCode)
  291. }
  292. cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
  293. if err := e.runCommand(ctx, t, call, i); err != nil {
  294. e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error())
  295. }
  296. }
  297. func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call, i int) error {
  298. cmd := t.Cmds[i]
  299. switch {
  300. case cmd.Task != "":
  301. reacquire := e.releaseConcurrencyLimit()
  302. defer reacquire()
  303. err := e.RunTask(ctx, &ast.Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
  304. if err != nil {
  305. return err
  306. }
  307. return nil
  308. case cmd.Cmd != "":
  309. if !shouldRunOnCurrentPlatform(cmd.Platforms) {
  310. e.Logger.VerboseOutf(logger.Yellow, "task: [%s] %s not for current platform - ignored\n", t.Name(), cmd.Cmd)
  311. return nil
  312. }
  313. if e.Verbose || (!call.Silent && !cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
  314. e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.Cmd)
  315. }
  316. if e.Dry {
  317. return nil
  318. }
  319. outputWrapper := e.Output
  320. if t.Interactive {
  321. outputWrapper = output.Interleaved{}
  322. }
  323. vars, err := e.Compiler.FastGetVariables(t, call)
  324. outputTemplater := &templater.Cache{Vars: vars}
  325. if err != nil {
  326. return fmt.Errorf("task: failed to get variables: %w", err)
  327. }
  328. stdOut, stdErr, close := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater)
  329. err = execext.RunCommand(ctx, &execext.RunCommandOptions{
  330. Command: cmd.Cmd,
  331. Dir: t.Dir,
  332. Env: env.Get(t),
  333. PosixOpts: slicesext.UniqueJoin(e.Taskfile.Set, t.Set, cmd.Set),
  334. BashOpts: slicesext.UniqueJoin(e.Taskfile.Shopt, t.Shopt, cmd.Shopt),
  335. Stdin: e.Stdin,
  336. Stdout: stdOut,
  337. Stderr: stdErr,
  338. })
  339. if closeErr := close(err); closeErr != nil {
  340. e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
  341. }
  342. if _, isExitError := interp.IsExitStatus(err); isExitError && cmd.IgnoreError {
  343. e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
  344. return nil
  345. }
  346. return err
  347. default:
  348. return nil
  349. }
  350. }
  351. func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func(ctx context.Context) error) error {
  352. h, err := e.GetHash(t)
  353. if err != nil {
  354. return err
  355. }
  356. if h == "" {
  357. return execute(ctx)
  358. }
  359. e.executionHashesMutex.Lock()
  360. if otherExecutionCtx, ok := e.executionHashes[h]; ok {
  361. e.executionHashesMutex.Unlock()
  362. e.Logger.VerboseErrf(logger.Magenta, "task: skipping execution of task: %s\n", h)
  363. // Release our execution slot to avoid blocking other tasks while we wait
  364. reacquire := e.releaseConcurrencyLimit()
  365. defer reacquire()
  366. <-otherExecutionCtx.Done()
  367. return nil
  368. }
  369. ctx, cancel := context.WithCancel(ctx)
  370. defer cancel()
  371. e.executionHashes[h] = ctx
  372. e.executionHashesMutex.Unlock()
  373. return execute(ctx)
  374. }
  375. // GetTask will return the task with the name matching the given call from the taskfile.
  376. // If no task is found, it will search for tasks with a matching alias.
  377. // If multiple tasks contain the same alias or no matches are found an error is returned.
  378. func (e *Executor) GetTask(call *ast.Call) (*ast.Task, error) {
  379. // Search for a matching task
  380. matchingTasks := e.Taskfile.Tasks.FindMatchingTasks(call)
  381. switch len(matchingTasks) {
  382. case 0: // Carry on
  383. case 1:
  384. if call.Vars == nil {
  385. call.Vars = &ast.Vars{}
  386. }
  387. call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards})
  388. return matchingTasks[0].Task, nil
  389. default:
  390. taskNames := make([]string, len(matchingTasks))
  391. for i, matchingTask := range matchingTasks {
  392. taskNames[i] = matchingTask.Task.Task
  393. }
  394. return nil, &errors.TaskNameConflictError{
  395. Call: call.Task,
  396. TaskNames: taskNames,
  397. }
  398. }
  399. // If didn't find one, search for a task with a matching alias
  400. var matchingTask *ast.Task
  401. var aliasedTasks []string
  402. for _, task := range e.Taskfile.Tasks.Values() {
  403. if slices.Contains(task.Aliases, call.Task) {
  404. aliasedTasks = append(aliasedTasks, task.Task)
  405. matchingTask = task
  406. }
  407. }
  408. // If we found multiple tasks
  409. if len(aliasedTasks) > 1 {
  410. return nil, &errors.TaskNameConflictError{
  411. Call: call.Task,
  412. TaskNames: aliasedTasks,
  413. }
  414. }
  415. // If we found no tasks
  416. if len(aliasedTasks) == 0 {
  417. didYouMean := ""
  418. if e.fuzzyModel != nil {
  419. didYouMean = e.fuzzyModel.SpellCheck(call.Task)
  420. }
  421. return nil, &errors.TaskNotFoundError{
  422. TaskName: call.Task,
  423. DidYouMean: didYouMean,
  424. }
  425. }
  426. return matchingTask, nil
  427. }
  428. type FilterFunc func(task *ast.Task) bool
  429. func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
  430. tasks := make([]*ast.Task, 0, e.Taskfile.Tasks.Len())
  431. // Create an error group to wait for each task to be compiled
  432. var g errgroup.Group
  433. // Filter tasks based on the given filter functions
  434. for _, task := range e.Taskfile.Tasks.Values() {
  435. var shouldFilter bool
  436. for _, filter := range filters {
  437. if filter(task) {
  438. shouldFilter = true
  439. }
  440. }
  441. if !shouldFilter {
  442. tasks = append(tasks, task)
  443. }
  444. }
  445. // Compile the list of tasks
  446. for i := range tasks {
  447. g.Go(func() error {
  448. compiledTask, err := e.FastCompiledTask(&ast.Call{Task: tasks[i].Task})
  449. if err != nil {
  450. return err
  451. }
  452. tasks[i] = compiledTask
  453. return nil
  454. })
  455. }
  456. // Wait for all the go routines to finish
  457. if err := g.Wait(); err != nil {
  458. return nil, err
  459. }
  460. // Sort the tasks
  461. if e.TaskSorter == nil {
  462. e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{}
  463. }
  464. e.TaskSorter.Sort(tasks)
  465. return tasks, nil
  466. }
  467. // FilterOutNoDesc removes all tasks that do not contain a description.
  468. func FilterOutNoDesc(task *ast.Task) bool {
  469. return task.Desc == ""
  470. }
  471. // FilterOutInternal removes all tasks that are marked as internal.
  472. func FilterOutInternal(task *ast.Task) bool {
  473. return task.Internal
  474. }
  475. func shouldRunOnCurrentPlatform(platforms []*ast.Platform) bool {
  476. if len(platforms) == 0 {
  477. return true
  478. }
  479. for _, p := range platforms {
  480. if (p.OS == "" || p.OS == runtime.GOOS) && (p.Arch == "" || p.Arch == runtime.GOARCH) {
  481. return true
  482. }
  483. }
  484. return false
  485. }