task.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  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. if t.Prompt != "" && !e.Dry {
  207. if err := e.Logger.Prompt(logger.Yellow, t.Prompt, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
  208. return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
  209. } else if errors.Is(err, logger.ErrPromptCancelled) {
  210. return &errors.TaskCancelledByUserError{TaskName: call.Task}
  211. } else if err != nil {
  212. return err
  213. }
  214. }
  215. if err := e.mkdir(t); err != nil {
  216. e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err)
  217. }
  218. var deferredExitCode uint8
  219. for i := range t.Cmds {
  220. if t.Cmds[i].Defer {
  221. defer e.runDeferred(t, call, i, &deferredExitCode)
  222. continue
  223. }
  224. if err := e.runCommand(ctx, t, call, i); err != nil {
  225. if err2 := e.statusOnError(t); err2 != nil {
  226. e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2)
  227. }
  228. exitCode, isExitError := interp.IsExitStatus(err)
  229. if isExitError {
  230. if t.IgnoreError {
  231. e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
  232. continue
  233. }
  234. deferredExitCode = exitCode
  235. }
  236. if call.Indirect {
  237. return err
  238. }
  239. return &errors.TaskRunError{TaskName: t.Task, Err: err}
  240. }
  241. }
  242. e.Logger.VerboseErrf(logger.Magenta, "task: %q finished\n", call.Task)
  243. return nil
  244. })
  245. }
  246. func (e *Executor) mkdir(t *ast.Task) error {
  247. if t.Dir == "" {
  248. return nil
  249. }
  250. mutex := e.mkdirMutexMap[t.Task]
  251. mutex.Lock()
  252. defer mutex.Unlock()
  253. if _, err := os.Stat(t.Dir); os.IsNotExist(err) {
  254. if err := os.MkdirAll(t.Dir, 0o755); err != nil {
  255. return err
  256. }
  257. }
  258. return nil
  259. }
  260. func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
  261. g, ctx := errgroup.WithContext(ctx)
  262. reacquire := e.releaseConcurrencyLimit()
  263. defer reacquire()
  264. for _, d := range t.Deps {
  265. d := d
  266. g.Go(func() error {
  267. err := e.RunTask(ctx, &ast.Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
  268. if err != nil {
  269. return err
  270. }
  271. return nil
  272. })
  273. }
  274. return g.Wait()
  275. }
  276. func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitCode *uint8) {
  277. ctx, cancel := context.WithCancel(context.Background())
  278. defer cancel()
  279. origTask, err := e.GetTask(call)
  280. if err != nil {
  281. return
  282. }
  283. cmd := t.Cmds[i]
  284. vars, _ := e.Compiler.GetVariables(origTask, call)
  285. cache := &templater.Cache{Vars: vars}
  286. extra := map[string]any{}
  287. if deferredExitCode != nil && *deferredExitCode > 0 {
  288. extra["EXIT_CODE"] = fmt.Sprintf("%d", *deferredExitCode)
  289. }
  290. cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
  291. if err := e.runCommand(ctx, t, call, i); err != nil {
  292. e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error())
  293. }
  294. }
  295. func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call, i int) error {
  296. cmd := t.Cmds[i]
  297. switch {
  298. case cmd.Task != "":
  299. reacquire := e.releaseConcurrencyLimit()
  300. defer reacquire()
  301. err := e.RunTask(ctx, &ast.Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
  302. if err != nil {
  303. return err
  304. }
  305. return nil
  306. case cmd.Cmd != "":
  307. if !shouldRunOnCurrentPlatform(cmd.Platforms) {
  308. e.Logger.VerboseOutf(logger.Yellow, "task: [%s] %s not for current platform - ignored\n", t.Name(), cmd.Cmd)
  309. return nil
  310. }
  311. if e.Verbose || (!call.Silent && !cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
  312. e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.Cmd)
  313. }
  314. if e.Dry {
  315. return nil
  316. }
  317. outputWrapper := e.Output
  318. if t.Interactive {
  319. outputWrapper = output.Interleaved{}
  320. }
  321. vars, err := e.Compiler.FastGetVariables(t, call)
  322. outputTemplater := &templater.Cache{Vars: vars}
  323. if err != nil {
  324. return fmt.Errorf("task: failed to get variables: %w", err)
  325. }
  326. stdOut, stdErr, close := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater)
  327. err = execext.RunCommand(ctx, &execext.RunCommandOptions{
  328. Command: cmd.Cmd,
  329. Dir: t.Dir,
  330. Env: env.Get(t),
  331. PosixOpts: slicesext.UniqueJoin(e.Taskfile.Set, t.Set, cmd.Set),
  332. BashOpts: slicesext.UniqueJoin(e.Taskfile.Shopt, t.Shopt, cmd.Shopt),
  333. Stdin: e.Stdin,
  334. Stdout: stdOut,
  335. Stderr: stdErr,
  336. })
  337. if closeErr := close(err); closeErr != nil {
  338. e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
  339. }
  340. if _, isExitError := interp.IsExitStatus(err); isExitError && cmd.IgnoreError {
  341. e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
  342. return nil
  343. }
  344. return err
  345. default:
  346. return nil
  347. }
  348. }
  349. func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func(ctx context.Context) error) error {
  350. h, err := e.GetHash(t)
  351. if err != nil {
  352. return err
  353. }
  354. if h == "" {
  355. return execute(ctx)
  356. }
  357. e.executionHashesMutex.Lock()
  358. if otherExecutionCtx, ok := e.executionHashes[h]; ok {
  359. e.executionHashesMutex.Unlock()
  360. e.Logger.VerboseErrf(logger.Magenta, "task: skipping execution of task: %s\n", h)
  361. // Release our execution slot to avoid blocking other tasks while we wait
  362. reacquire := e.releaseConcurrencyLimit()
  363. defer reacquire()
  364. <-otherExecutionCtx.Done()
  365. return nil
  366. }
  367. ctx, cancel := context.WithCancel(ctx)
  368. defer cancel()
  369. e.executionHashes[h] = ctx
  370. e.executionHashesMutex.Unlock()
  371. return execute(ctx)
  372. }
  373. // GetTask will return the task with the name matching the given call from the taskfile.
  374. // If no task is found, it will search for tasks with a matching alias.
  375. // If multiple tasks contain the same alias or no matches are found an error is returned.
  376. func (e *Executor) GetTask(call *ast.Call) (*ast.Task, error) {
  377. // Search for a matching task
  378. matchingTasks := e.Taskfile.Tasks.FindMatchingTasks(call)
  379. switch len(matchingTasks) {
  380. case 0: // Carry on
  381. case 1:
  382. if call.Vars == nil {
  383. call.Vars = &ast.Vars{}
  384. }
  385. call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards})
  386. return matchingTasks[0].Task, nil
  387. default:
  388. taskNames := make([]string, len(matchingTasks))
  389. for i, matchingTask := range matchingTasks {
  390. taskNames[i] = matchingTask.Task.Task
  391. }
  392. return nil, &errors.TaskNameConflictError{
  393. Call: call.Task,
  394. TaskNames: taskNames,
  395. }
  396. }
  397. // If didn't find one, search for a task with a matching alias
  398. var matchingTask *ast.Task
  399. var aliasedTasks []string
  400. for _, task := range e.Taskfile.Tasks.Values() {
  401. if slices.Contains(task.Aliases, call.Task) {
  402. aliasedTasks = append(aliasedTasks, task.Task)
  403. matchingTask = task
  404. }
  405. }
  406. // If we found multiple tasks
  407. if len(aliasedTasks) > 1 {
  408. return nil, &errors.TaskNameConflictError{
  409. Call: call.Task,
  410. TaskNames: aliasedTasks,
  411. }
  412. }
  413. // If we found no tasks
  414. if len(aliasedTasks) == 0 {
  415. didYouMean := ""
  416. if e.fuzzyModel != nil {
  417. didYouMean = e.fuzzyModel.SpellCheck(call.Task)
  418. }
  419. return nil, &errors.TaskNotFoundError{
  420. TaskName: call.Task,
  421. DidYouMean: didYouMean,
  422. }
  423. }
  424. return matchingTask, nil
  425. }
  426. type FilterFunc func(task *ast.Task) bool
  427. func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
  428. tasks := make([]*ast.Task, 0, e.Taskfile.Tasks.Len())
  429. // Create an error group to wait for each task to be compiled
  430. var g errgroup.Group
  431. // Filter tasks based on the given filter functions
  432. for _, task := range e.Taskfile.Tasks.Values() {
  433. var shouldFilter bool
  434. for _, filter := range filters {
  435. if filter(task) {
  436. shouldFilter = true
  437. }
  438. }
  439. if !shouldFilter {
  440. tasks = append(tasks, task)
  441. }
  442. }
  443. // Compile the list of tasks
  444. for i := range tasks {
  445. g.Go(func() error {
  446. compiledTask, err := e.FastCompiledTask(&ast.Call{Task: tasks[i].Task})
  447. if err != nil {
  448. return err
  449. }
  450. tasks[i] = compiledTask
  451. return nil
  452. })
  453. }
  454. // Wait for all the go routines to finish
  455. if err := g.Wait(); err != nil {
  456. return nil, err
  457. }
  458. // Sort the tasks
  459. if e.TaskSorter == nil {
  460. e.TaskSorter = &sort.AlphaNumericWithRootTasksFirst{}
  461. }
  462. e.TaskSorter.Sort(tasks)
  463. return tasks, nil
  464. }
  465. // FilterOutNoDesc removes all tasks that do not contain a description.
  466. func FilterOutNoDesc(task *ast.Task) bool {
  467. return task.Desc == ""
  468. }
  469. // FilterOutInternal removes all tasks that are marked as internal.
  470. func FilterOutInternal(task *ast.Task) bool {
  471. return task.Internal
  472. }
  473. func shouldRunOnCurrentPlatform(platforms []*ast.Platform) bool {
  474. if len(platforms) == 0 {
  475. return true
  476. }
  477. for _, p := range platforms {
  478. if (p.OS == "" || p.OS == runtime.GOOS) && (p.Arch == "" || p.Arch == runtime.GOARCH) {
  479. return true
  480. }
  481. }
  482. return false
  483. }