2023-12-29 22:32:03 +02:00
package taskfile
2018-07-22 21:05:47 +02:00
import (
2023-09-12 23:42:54 +02:00
"context"
2018-07-22 21:05:47 +02:00
"fmt"
"os"
"path/filepath"
2023-11-17 22:51:10 +02:00
"time"
2018-07-22 21:05:47 +02:00
2023-09-12 23:42:54 +02:00
"gopkg.in/yaml.v3"
2023-04-15 22:22:25 +02:00
"github.com/go-task/task/v3/errors"
2022-08-06 23:19:07 +02:00
"github.com/go-task/task/v3/internal/filepathext"
2023-09-12 23:42:54 +02:00
"github.com/go-task/task/v3/internal/logger"
2022-12-06 02:58:20 +02:00
"github.com/go-task/task/v3/internal/sysinfo"
2021-01-07 16:48:33 +02:00
"github.com/go-task/task/v3/internal/templater"
2023-12-29 22:32:03 +02:00
"github.com/go-task/task/v3/taskfile/ast"
2018-07-22 21:05:47 +02:00
)
2019-01-21 11:56:14 +02:00
var (
2020-08-16 00:12:39 +02:00
// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs
2020-08-04 00:18:38 +02:00
ErrIncludedTaskfilesCantHaveDotenvs = errors . New ( "task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile" )
2021-12-04 17:37:52 +02:00
2022-02-19 23:24:43 +02:00
defaultTaskfiles = [ ] string {
"Taskfile.yml" ,
2023-06-17 19:38:53 +02:00
"taskfile.yml" ,
2022-02-19 23:24:43 +02:00
"Taskfile.yaml" ,
2023-06-17 19:38:53 +02:00
"taskfile.yaml" ,
2022-02-19 23:24:43 +02:00
"Taskfile.dist.yml" ,
2023-06-17 19:38:53 +02:00
"taskfile.dist.yml" ,
2022-02-19 23:24:43 +02:00
"Taskfile.dist.yaml" ,
2023-06-17 19:38:53 +02:00
"taskfile.dist.yaml" ,
2022-02-19 23:24:43 +02:00
}
2019-01-21 11:56:14 +02:00
)
2018-10-13 22:52:09 +02:00
2023-09-12 23:42:54 +02:00
func readTaskfile (
node Node ,
download ,
offline bool ,
2023-11-17 22:51:10 +02:00
timeout time . Duration ,
2023-09-12 23:42:54 +02:00
tempDir string ,
l * logger . Logger ,
2023-12-29 22:32:03 +02:00
) ( * ast . Taskfile , error ) {
2023-09-12 23:42:54 +02:00
var b [ ] byte
2023-09-14 22:52:56 +02:00
var err error
var cache * Cache
2023-09-14 23:57:46 +02:00
2023-09-14 22:52:56 +02:00
if node . Remote ( ) {
cache , err = NewCache ( tempDir )
if err != nil {
return nil , err
}
2023-09-12 23:42:54 +02:00
}
2023-11-17 22:51:10 +02:00
// If the file is remote and we're in offline mode, check if we have a cached copy
if node . Remote ( ) && offline {
if b , err = cache . read ( node ) ; errors . Is ( err , os . ErrNotExist ) {
return nil , & errors . TaskfileCacheNotFound { URI : node . Location ( ) }
} else if err != nil {
2023-09-12 23:42:54 +02:00
return nil , err
}
2023-11-17 22:51:10 +02:00
l . VerboseOutf ( logger . Magenta , "task: [%s] Fetched cached copy\n" , node . Location ( ) )
2023-09-12 23:42:54 +02:00
2023-11-17 22:51:10 +02:00
} else {
2023-09-12 23:42:54 +02:00
2023-11-17 22:51:10 +02:00
downloaded := false
ctx , cf := context . WithTimeout ( context . Background ( ) , timeout )
defer cf ( )
2023-09-12 23:42:54 +02:00
2023-11-17 22:51:10 +02:00
// Read the file
b , err = node . Read ( ctx )
// If we timed out then we likely have a network issue
if node . Remote ( ) && errors . Is ( ctx . Err ( ) , context . DeadlineExceeded ) {
// If a download was requested, then we can't use a cached copy
if download {
return nil , & errors . TaskfileNetworkTimeout { URI : node . Location ( ) , Timeout : timeout }
}
// Search for any cached copies
if b , err = cache . read ( node ) ; errors . Is ( err , os . ErrNotExist ) {
return nil , & errors . TaskfileNetworkTimeout { URI : node . Location ( ) , Timeout : timeout , CheckedCache : true }
} else if err != nil {
return nil , err
}
l . VerboseOutf ( logger . Magenta , "task: [%s] Network timeout. Fetched cached copy\n" , node . Location ( ) )
} else if err != nil {
2023-09-12 23:42:54 +02:00
return nil , err
2023-11-17 22:51:10 +02:00
} else {
downloaded = true
2023-09-12 23:42:54 +02:00
}
// If the node was remote, we need to check the checksum
2023-11-17 22:51:10 +02:00
if node . Remote ( ) && downloaded {
2023-09-12 23:42:54 +02:00
l . VerboseOutf ( logger . Magenta , "task: [%s] Fetched remote copy\n" , node . Location ( ) )
// Get the checksums
checksum := checksum ( b )
cachedChecksum := cache . readChecksum ( node )
2023-10-07 23:55:43 +02:00
var msg string
2023-09-12 23:42:54 +02:00
if cachedChecksum == "" {
2023-10-07 23:55:43 +02:00
// If the checksum doesn't exist, prompt the user to continue
msg = fmt . Sprintf ( "The task you are attempting to run depends on the remote Taskfile at %q.\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?" , node . Location ( ) )
2023-09-12 23:42:54 +02:00
} else if checksum != cachedChecksum {
// If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue
2023-10-07 23:55:43 +02:00
msg = fmt . Sprintf ( "The Taskfile at %q has changed since you last used it!\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?" , node . Location ( ) )
}
if msg != "" {
if err := l . Prompt ( logger . Yellow , msg , "n" , "y" , "yes" ) ; errors . Is ( err , logger . ErrPromptCancelled ) {
2023-09-12 23:42:54 +02:00
return nil , & errors . TaskfileNotTrustedError { URI : node . Location ( ) }
2023-10-07 23:55:43 +02:00
} else if err != nil {
return nil , err
2023-09-12 23:42:54 +02:00
}
}
2023-11-17 22:51:10 +02:00
// If the hash has changed (or is new)
2023-09-12 23:42:54 +02:00
if checksum != cachedChecksum {
2023-11-17 22:51:10 +02:00
// Store the checksum
2023-09-12 23:42:54 +02:00
if err := cache . writeChecksum ( node , checksum ) ; err != nil {
return nil , err
}
2023-11-17 22:51:10 +02:00
// Cache the file
l . VerboseOutf ( logger . Magenta , "task: [%s] Caching downloaded file\n" , node . Location ( ) )
if err = cache . write ( node , b ) ; err != nil {
return nil , err
}
2023-09-12 23:42:54 +02:00
}
}
}
2023-12-29 22:32:03 +02:00
var t ast . Taskfile
2023-09-12 23:42:54 +02:00
if err := yaml . Unmarshal ( b , & t ) ; err != nil {
return nil , & errors . TaskfileInvalidError { URI : filepathext . TryAbsToRel ( node . Location ( ) ) , Err : err }
}
t . Location = node . Location ( )
return & t , nil
}
2023-12-29 22:32:03 +02:00
// Read reads a Read for a given directory
// Uses current dir when dir is left empty. Uses Read.yml
// or Read.yaml when entrypoint is left empty
func Read (
2023-09-12 23:42:54 +02:00
node Node ,
insecure bool ,
download bool ,
offline bool ,
2023-11-17 22:51:10 +02:00
timeout time . Duration ,
2023-09-12 23:42:54 +02:00
tempDir string ,
l * logger . Logger ,
2023-12-29 22:32:03 +02:00
) ( * ast . Taskfile , error ) {
var _taskfile func ( Node ) ( * ast . Taskfile , error )
_taskfile = func ( node Node ) ( * ast . Taskfile , error ) {
2023-11-17 22:51:10 +02:00
t , err := readTaskfile ( node , download , offline , timeout , tempDir , l )
2021-12-04 17:37:52 +02:00
if err != nil {
2023-09-02 22:24:01 +02:00
return nil , err
2021-12-04 17:37:52 +02:00
}
2022-07-26 00:10:16 +02:00
2023-09-19 20:21:40 +02:00
// Check that the Taskfile is set and has a schema version
if t == nil || t . Version == nil {
return nil , & errors . TaskfileVersionNotDefined { URI : node . Location ( ) }
}
2023-09-02 22:24:01 +02:00
// Annotate any included Taskfile reference with a base directory for resolving relative paths
if node , isFileNode := node . ( * FileNode ) ; isFileNode {
2024-01-04 02:04:53 +02:00
_ = t . Includes . Range ( func ( namespace string , include ast . Include ) error {
2023-09-02 22:24:01 +02:00
// Set the base directory for resolving relative paths, but only if not already set
2024-01-04 02:04:53 +02:00
if include . BaseDir == "" {
include . BaseDir = node . Dir
t . Includes . Set ( namespace , include )
2023-09-02 22:24:01 +02:00
}
return nil
} )
2022-07-26 00:10:16 +02:00
}
2024-01-04 02:04:53 +02:00
err = t . Includes . Range ( func ( namespace string , include ast . Include ) error {
2023-12-29 22:26:02 +02:00
tr := templater . Templater { Vars : t . Vars }
2024-01-04 02:04:53 +02:00
include = ast . Include {
2024-01-04 02:17:30 +02:00
Namespace : include . Namespace ,
2024-01-04 02:04:53 +02:00
Taskfile : tr . Replace ( include . Taskfile ) ,
Dir : tr . Replace ( include . Dir ) ,
Optional : include . Optional ,
Internal : include . Internal ,
Aliases : include . Aliases ,
AdvancedImport : include . AdvancedImport ,
Vars : include . Vars ,
BaseDir : include . BaseDir ,
2023-12-29 22:26:02 +02:00
}
if err := tr . Err ( ) ; err != nil {
return err
2020-05-17 21:03:03 +02:00
}
2023-09-02 22:24:01 +02:00
2024-01-04 02:04:53 +02:00
uri , err := include . FullTaskfilePath ( )
2023-09-12 23:42:54 +02:00
if err != nil {
return err
}
includeReaderNode , err := NewNode ( uri , insecure ,
WithParent ( node ) ,
2024-01-04 02:04:53 +02:00
WithOptional ( include . Optional ) ,
2023-09-12 23:42:54 +02:00
)
2023-09-02 22:24:01 +02:00
if err != nil {
2024-01-04 02:04:53 +02:00
if include . Optional {
2023-09-02 22:24:01 +02:00
return nil
}
2021-01-01 23:27:50 +02:00
return err
2020-05-17 21:03:03 +02:00
}
2023-09-02 22:24:01 +02:00
if err := checkCircularIncludes ( includeReaderNode ) ; err != nil {
return err
}
2022-07-26 00:10:16 +02:00
2023-09-02 22:24:01 +02:00
includedTaskfile , err := _taskfile ( includeReaderNode )
if err != nil {
2024-01-04 02:04:53 +02:00
if include . Optional {
2023-09-02 22:24:01 +02:00
return nil
}
return err
2021-09-25 14:40:03 +02:00
}
2021-12-04 17:37:52 +02:00
2023-12-29 22:26:02 +02:00
if len ( includedTaskfile . Dotenv ) > 0 {
2023-09-02 22:24:01 +02:00
return ErrIncludedTaskfilesCantHaveDotenvs
}
2022-01-15 05:38:37 +02:00
2024-01-04 02:04:53 +02:00
if include . AdvancedImport {
dir , err := include . FullDirPath ( )
2023-09-02 22:24:01 +02:00
if err != nil {
return err
}
2022-01-16 06:34:59 +02:00
2023-09-02 22:24:01 +02:00
// nolint: errcheck
2023-12-29 22:32:03 +02:00
includedTaskfile . Vars . Range ( func ( k string , v ast . Var ) error {
2023-09-02 22:24:01 +02:00
o := v
o . Dir = dir
includedTaskfile . Vars . Set ( k , o )
return nil
} )
// nolint: errcheck
2023-12-29 22:32:03 +02:00
includedTaskfile . Env . Range ( func ( k string , v ast . Var ) error {
2023-09-02 22:24:01 +02:00
o := v
o . Dir = dir
includedTaskfile . Env . Set ( k , o )
return nil
} )
for _ , task := range includedTaskfile . Tasks . Values ( ) {
task . Dir = filepathext . SmartJoin ( dir , task . Dir )
if task . IncludeVars == nil {
2023-12-29 22:32:03 +02:00
task . IncludeVars = & ast . Vars { }
2023-09-02 22:24:01 +02:00
}
2024-01-04 02:04:53 +02:00
task . IncludeVars . Merge ( include . Vars )
2023-09-02 22:24:01 +02:00
task . IncludedTaskfileVars = includedTaskfile . Vars
2024-01-04 02:04:53 +02:00
task . IncludedTaskfile = & include
2023-09-02 22:24:01 +02:00
}
2022-01-15 05:38:37 +02:00
}
2020-01-29 09:03:06 +02:00
2024-01-04 02:17:30 +02:00
if err = Merge ( t , includedTaskfile , & include ) ; err != nil {
2022-07-26 00:10:16 +02:00
return err
}
2023-09-02 22:24:01 +02:00
return nil
} )
if err != nil {
return nil , err
}
for _ , task := range t . Tasks . Values ( ) {
// If the task is not defined, create a new one
if task == nil {
2023-12-29 22:32:03 +02:00
task = & ast . Task { }
2020-05-17 20:42:27 +02:00
}
2023-09-02 22:24:01 +02:00
// Set the location of the taskfile for each task
if task . Location . Taskfile == "" {
task . Location . Taskfile = t . Location
2020-05-17 20:42:27 +02:00
}
2018-07-22 21:05:47 +02:00
}
2022-12-23 02:27:16 +02:00
2023-09-02 22:24:01 +02:00
return t , nil
2022-10-07 12:18:53 +02:00
}
2023-09-02 22:24:01 +02:00
return _taskfile ( node )
2018-07-22 21:05:47 +02:00
}
2021-12-04 17:37:52 +02:00
2023-09-14 23:57:46 +02:00
// Exists will check if a file at the given path Exists. If it does, it will
2023-09-12 23:42:54 +02:00
// return the path to it. If it does not, it will search the search for any
// files at the given path with any of the default Taskfile files names. If any
// of these match a file, the first matching path will be returned. If no files
// are found, an error will be returned.
2023-09-14 23:57:46 +02:00
func Exists ( path string ) ( string , error ) {
2021-12-04 17:37:52 +02:00
fi , err := os . Stat ( path )
if err != nil {
return "" , err
}
if fi . Mode ( ) . IsRegular ( ) {
2023-09-15 00:15:54 +02:00
return filepath . Abs ( path )
2021-12-04 17:37:52 +02:00
}
for _ , n := range defaultTaskfiles {
2022-08-06 23:19:07 +02:00
fpath := filepathext . SmartJoin ( path , n )
2021-12-04 17:37:52 +02:00
if _ , err := os . Stat ( fpath ) ; err == nil {
2023-09-15 00:15:54 +02:00
return filepath . Abs ( fpath )
2021-12-04 17:37:52 +02:00
}
}
2023-09-02 22:24:01 +02:00
return "" , errors . TaskfileNotFoundError { URI : path , Walk : false }
2021-12-04 17:37:52 +02:00
}
2022-01-16 06:34:59 +02:00
2023-09-14 23:57:46 +02:00
// ExistsWalk will check if a file at the given path exists by calling the
2023-09-12 23:42:54 +02:00
// exists function. If a file is not found, it will walk up the directory tree
// calling the exists function until it finds a file or reaches the root
// directory. On supported operating systems, it will also check if the user ID
// of the directory changes and abort if it does.
2023-09-14 23:57:46 +02:00
func ExistsWalk ( path string ) ( string , error ) {
2022-12-06 02:58:20 +02:00
origPath := path
owner , err := sysinfo . Owner ( path )
if err != nil {
return "" , err
}
for {
2023-09-14 23:57:46 +02:00
fpath , err := Exists ( path )
2022-12-06 02:58:20 +02:00
if err == nil {
return fpath , nil
}
// Get the parent path/user id
parentPath := filepath . Dir ( path )
parentOwner , err := sysinfo . Owner ( parentPath )
if err != nil {
return "" , err
}
// Error if we reached the root directory and still haven't found a file
// OR if the user id of the directory changes
if path == parentPath || ( parentOwner != owner ) {
2023-09-02 22:24:01 +02:00
return "" , errors . TaskfileNotFoundError { URI : origPath , Walk : false }
2022-12-06 02:58:20 +02:00
}
owner = parentOwner
path = parentPath
}
}
2023-09-02 22:24:01 +02:00
func checkCircularIncludes ( node Node ) error {
2022-01-16 06:34:59 +02:00
if node == nil {
2022-02-04 05:12:58 +02:00
return errors . New ( "task: failed to check for include cycle: node was nil" )
2022-01-16 06:34:59 +02:00
}
2023-09-02 22:24:01 +02:00
if node . Parent ( ) == nil {
2022-02-04 05:12:58 +02:00
return errors . New ( "task: failed to check for include cycle: node.Parent was nil" )
2022-01-16 06:34:59 +02:00
}
2023-03-31 21:13:29 +02:00
curNode := node
2023-09-02 22:24:01 +02:00
location := node . Location ( )
for curNode . Parent ( ) != nil {
curNode = curNode . Parent ( )
curLocation := curNode . Location ( )
if curLocation == location {
2022-02-04 05:12:58 +02:00
return fmt . Errorf ( "task: include cycle detected between %s <--> %s" ,
2023-09-02 22:24:01 +02:00
curLocation ,
node . Parent ( ) . Location ( ) ,
2022-01-16 06:34:59 +02:00
)
}
}
return nil
}