2018-07-22 16:05:47 -03:00
package read
import (
"fmt"
"os"
"path/filepath"
"runtime"
2020-03-28 11:27:49 -03:00
"gopkg.in/yaml.v3"
2021-01-07 11:48:33 -03:00
2023-04-15 21:22:25 +01:00
"github.com/go-task/task/v3/errors"
2022-08-06 18:19:07 -03:00
"github.com/go-task/task/v3/internal/filepathext"
2022-12-06 00:58:20 +00:00
"github.com/go-task/task/v3/internal/sysinfo"
2021-01-07 11:48:33 -03:00
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile"
2018-07-22 16:05:47 -03:00
)
2019-01-21 14:56:14 +05:00
var (
2020-08-15 19:12:39 -03:00
// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs
2020-08-03 16:18:38 -06: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 18:24:43 -03:00
defaultTaskfiles = [ ] string {
"Taskfile.yml" ,
2023-06-17 18:38:53 +01:00
"taskfile.yml" ,
2022-02-19 18:24:43 -03:00
"Taskfile.yaml" ,
2023-06-17 18:38:53 +01:00
"taskfile.yaml" ,
2022-02-19 18:24:43 -03:00
"Taskfile.dist.yml" ,
2023-06-17 18:38:53 +01:00
"taskfile.dist.yml" ,
2022-02-19 18:24:43 -03:00
"Taskfile.dist.yaml" ,
2023-06-17 18:38:53 +01:00
"taskfile.dist.yaml" ,
2022-02-19 18:24:43 -03:00
}
2019-01-21 14:56:14 +05:00
)
2018-10-13 17:52:09 -03:00
2022-01-14 22:38:37 -05:00
type ReaderNode struct {
Dir string
Entrypoint string
Optional bool
Parent * ReaderNode
}
2018-07-22 16:05:47 -03:00
// Taskfile reads a Taskfile for a given directory
2021-12-04 17:37:52 +02:00
// Uses current dir when dir is left empty. Uses Taskfile.yml
// or Taskfile.yaml when entrypoint is left empty
2022-12-06 00:58:20 +00:00
func Taskfile ( readerNode * ReaderNode ) ( * taskfile . Taskfile , string , error ) {
2022-01-14 22:38:37 -05:00
if readerNode . Dir == "" {
2021-12-04 17:37:52 +02:00
d , err := os . Getwd ( )
if err != nil {
2022-12-06 00:58:20 +00:00
return nil , "" , err
2021-12-04 17:37:52 +02:00
}
2022-01-14 22:38:37 -05:00
readerNode . Dir = d
2018-09-22 17:29:18 -03:00
}
2022-07-26 10:10:16 +12:00
2022-12-06 00:58:20 +00:00
path , err := existsWalk ( filepathext . SmartJoin ( readerNode . Dir , readerNode . Entrypoint ) )
2021-12-04 17:37:52 +02:00
if err != nil {
2022-12-06 00:58:20 +00:00
return nil , "" , err
2021-12-04 17:37:52 +02:00
}
2022-12-06 00:58:20 +00:00
readerNode . Dir = filepath . Dir ( path )
2022-01-14 22:38:37 -05:00
readerNode . Entrypoint = filepath . Base ( path )
2021-12-04 17:37:52 +02:00
2018-07-22 16:05:47 -03:00
t , err := readTaskfile ( path )
if err != nil {
2022-12-06 00:58:20 +00:00
return nil , "" , err
2018-07-22 16:05:47 -03:00
}
2022-07-26 10:10:16 +12:00
// Annotate any included Taskfile reference with a base directory for resolving relative paths
_ = t . Includes . Range ( func ( key string , includedFile taskfile . IncludedTaskfile ) error {
// Set the base directory for resolving relative paths, but only if not already set
if includedFile . BaseDir == "" {
includedFile . BaseDir = readerNode . Dir
t . Includes . Set ( key , includedFile )
}
return nil
} )
2021-01-01 18:27:50 -03:00
err = t . Includes . Range ( func ( namespace string , includedTask taskfile . IncludedTaskfile ) error {
2023-02-08 10:21:43 +00:00
if t . Version . Compare ( taskfile . V3 ) >= 0 {
2022-11-23 17:58:08 +00:00
tr := templater . Templater { Vars : t . Vars , RemoveNoValue : true }
2020-05-17 16:03:03 -03:00
includedTask = taskfile . IncludedTaskfile {
Taskfile : tr . Replace ( includedTask . Taskfile ) ,
Dir : tr . Replace ( includedTask . Dir ) ,
2021-08-11 17:28:44 +01:00
Optional : includedTask . Optional ,
2022-07-22 02:16:14 +00:00
Internal : includedTask . Internal ,
2022-10-02 05:07:58 +00:00
Aliases : includedTask . Aliases ,
2020-05-17 16:03:03 -03:00
AdvancedImport : includedTask . AdvancedImport ,
2022-02-23 16:53:46 -06:00
Vars : includedTask . Vars ,
2022-07-26 10:10:16 +12:00
BaseDir : includedTask . BaseDir ,
2020-05-17 16:03:03 -03:00
}
if err := tr . Err ( ) ; err != nil {
2021-01-01 18:27:50 -03:00
return err
2020-05-17 16:03:03 -03:00
}
}
2022-07-26 10:10:16 +12:00
path , err := includedTask . FullTaskfilePath ( )
2021-09-05 10:57:49 -04:00
if err != nil {
return err
}
2022-07-26 10:10:16 +12:00
2021-12-04 17:37:52 +02:00
path , err = exists ( path )
2018-09-09 22:29:29 -03:00
if err != nil {
2021-09-25 09:40:03 -03:00
if includedTask . Optional {
return nil
}
2021-01-01 18:27:50 -03:00
return err
2018-09-09 22:29:29 -03:00
}
2021-12-04 17:37:52 +02:00
2022-01-15 23:34:59 -05:00
includeReaderNode := & ReaderNode {
2022-01-14 22:38:37 -05:00
Dir : filepath . Dir ( path ) ,
Entrypoint : filepath . Base ( path ) ,
Parent : readerNode ,
Optional : includedTask . Optional ,
}
2022-01-15 23:34:59 -05:00
if err := checkCircularIncludes ( includeReaderNode ) ; err != nil {
2021-01-01 18:27:50 -03:00
return err
2018-09-09 22:29:29 -03:00
}
2022-01-15 23:34:59 -05:00
2022-12-06 00:58:20 +00:00
includedTaskfile , _ , err := Taskfile ( includeReaderNode )
2018-09-09 22:29:29 -03:00
if err != nil {
2022-01-14 22:38:37 -05:00
if includedTask . Optional {
return nil
}
2021-01-01 18:27:50 -03:00
return err
2018-10-13 17:52:09 -03:00
}
2020-01-29 10:03:06 +03:00
2023-02-08 10:21:43 +00:00
if t . Version . Compare ( taskfile . V3 ) >= 0 && len ( includedTaskfile . Dotenv ) > 0 {
2021-01-01 18:27:50 -03:00
return ErrIncludedTaskfilesCantHaveDotenvs
2020-08-03 16:18:38 -06:00
}
2020-02-15 17:24:06 +03:00
if includedTask . AdvancedImport {
2022-07-26 10:10:16 +12:00
dir , err := includedTask . FullDirPath ( )
if err != nil {
return err
}
2023-04-06 12:07:57 +01:00
// nolint: errcheck
includedTaskfile . Vars . Range ( func ( k string , v taskfile . Var ) error {
2021-01-09 12:09:23 -03:00
o := v
2022-07-26 10:10:16 +12:00
o . Dir = dir
2023-04-06 12:07:57 +01:00
includedTaskfile . Vars . Set ( k , o )
return nil
} )
// nolint: errcheck
includedTaskfile . Env . Range ( func ( k string , v taskfile . Var ) error {
2021-01-09 12:09:23 -03:00
o := v
2022-07-26 10:10:16 +12:00
o . Dir = dir
2023-04-06 12:07:57 +01:00
includedTaskfile . Env . Set ( k , o )
return nil
} )
2021-01-09 12:09:23 -03:00
2023-04-06 12:07:57 +01:00
for _ , task := range includedTaskfile . Tasks . Values ( ) {
2022-08-06 18:19:07 -03:00
task . Dir = filepathext . SmartJoin ( dir , task . Dir )
2023-07-08 08:42:38 -06:00
if task . IncludeVars == nil {
task . IncludeVars = & taskfile . Vars { }
}
task . IncludeVars . Merge ( includedTask . Vars )
2022-03-19 18:41:03 -03:00
task . IncludedTaskfileVars = includedTaskfile . Vars
2022-09-03 18:14:54 -03:00
task . IncludedTaskfile = & includedTask
2020-02-15 16:40:42 +03:00
}
2020-01-29 10:03:06 +03:00
}
2022-10-02 05:07:58 +00:00
if err = taskfile . Merge ( t , includedTaskfile , & includedTask , namespace ) ; err != nil {
2021-01-01 18:27:50 -03:00
return err
2018-09-09 22:29:29 -03:00
}
2022-11-02 14:27:15 +00:00
2023-04-06 12:07:57 +01:00
if includedTaskfile . Tasks . Get ( "default" ) != nil && t . Tasks . Get ( namespace ) == nil {
2022-11-02 14:27:15 +00:00
defaultTaskName := fmt . Sprintf ( "%s:default" , namespace )
2023-04-06 12:07:57 +01:00
task := t . Tasks . Get ( defaultTaskName )
task . Aliases = append ( task . Aliases , namespace )
task . Aliases = append ( task . Aliases , includedTask . Aliases ... )
t . Tasks . Set ( defaultTaskName , task )
2022-11-02 14:27:15 +00:00
}
2021-01-01 18:27:50 -03:00
return nil
} )
if err != nil {
2022-12-06 00:58:20 +00:00
return nil , "" , err
2018-09-09 22:29:29 -03:00
}
2023-02-08 10:21:43 +00:00
if t . Version . Compare ( taskfile . V3 ) < 0 {
2022-08-06 18:19:07 -03:00
path = filepathext . SmartJoin ( readerNode . Dir , fmt . Sprintf ( "Taskfile_%s.yml" , runtime . GOOS ) )
2020-05-17 15:42:27 -03:00
if _ , err = os . Stat ( path ) ; err == nil {
osTaskfile , err := readTaskfile ( path )
if err != nil {
2022-12-06 00:58:20 +00:00
return nil , "" , err
2020-05-17 15:42:27 -03:00
}
2022-10-02 05:07:58 +00:00
if err = taskfile . Merge ( t , osTaskfile , nil ) ; err != nil {
2022-12-06 00:58:20 +00:00
return nil , "" , err
2020-05-17 15:42:27 -03:00
}
2018-07-22 16:05:47 -03:00
}
}
2023-03-17 12:34:06 +00:00
// Set the location of the Taskfile
t . Location = path
2023-04-06 12:07:57 +01:00
for _ , task := range t . Tasks . Values ( ) {
2023-03-17 12:34:06 +00:00
// If the task is not defined, create a new one
2020-10-12 21:03:13 -03:00
if task == nil {
task = & taskfile . Task { }
}
2023-03-17 12:34:06 +00:00
// Set the location of the taskfile for each task
if task . Location . Taskfile == "" {
task . Location . Taskfile = path
}
2018-07-22 16:05:47 -03:00
}
2022-12-06 00:58:20 +00:00
return t , readerNode . Dir , nil
2018-07-22 16:05:47 -03:00
}
func readTaskfile ( file string ) ( * taskfile . Taskfile , error ) {
f , err := os . Open ( file )
if err != nil {
return nil , err
}
2022-12-22 21:23:17 -03:00
defer f . Close ( )
2022-12-22 21:27:16 -03:00
2018-07-22 16:05:47 -03:00
var t taskfile . Taskfile
2022-10-07 10:18:53 +00:00
if err := yaml . NewDecoder ( f ) . Decode ( & t ) ; err != nil {
2023-04-15 21:22:25 +01:00
return nil , & errors . TaskfileInvalidError { FilePath : filepathext . TryAbsToRel ( file ) , Err : err }
2022-10-07 10:18:53 +00:00
}
return & t , nil
2018-07-22 16:05:47 -03:00
}
2021-12-04 17:37:52 +02:00
func exists ( path string ) ( string , error ) {
fi , err := os . Stat ( path )
if err != nil {
return "" , err
}
if fi . Mode ( ) . IsRegular ( ) {
2022-08-06 18:19:07 -03:00
return path , nil
2021-12-04 17:37:52 +02:00
}
for _ , n := range defaultTaskfiles {
2022-08-06 18:19:07 -03:00
fpath := filepathext . SmartJoin ( path , n )
2021-12-04 17:37:52 +02:00
if _ , err := os . Stat ( fpath ) ; err == nil {
2022-08-06 18:19:07 -03:00
return fpath , nil
2021-12-04 17:37:52 +02:00
}
}
2023-04-15 21:22:25 +01:00
return "" , errors . TaskfileNotFoundError { Dir : path , Walk : false }
2021-12-04 17:37:52 +02:00
}
2022-01-15 23:34:59 -05:00
2022-12-06 00:58:20 +00:00
func existsWalk ( path string ) ( string , error ) {
origPath := path
owner , err := sysinfo . Owner ( path )
if err != nil {
return "" , err
}
for {
fpath , err := exists ( path )
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-04-15 21:22:25 +01:00
return "" , errors . TaskfileNotFoundError { Dir : origPath , Walk : false }
2022-12-06 00:58:20 +00:00
}
owner = parentOwner
path = parentPath
}
}
2022-01-15 23:34:59 -05:00
func checkCircularIncludes ( node * ReaderNode ) error {
if node == nil {
2022-02-03 22:12:58 -05:00
return errors . New ( "task: failed to check for include cycle: node was nil" )
2022-01-15 23:34:59 -05:00
}
if node . Parent == nil {
2022-02-03 22:12:58 -05:00
return errors . New ( "task: failed to check for include cycle: node.Parent was nil" )
2022-01-15 23:34:59 -05:00
}
2023-03-31 19:13:29 +00:00
curNode := node
basePath := filepathext . SmartJoin ( node . Dir , node . Entrypoint )
2022-01-15 23:34:59 -05:00
for curNode . Parent != nil {
curNode = curNode . Parent
2022-08-06 18:19:07 -03:00
curPath := filepathext . SmartJoin ( curNode . Dir , curNode . Entrypoint )
2022-01-15 23:34:59 -05:00
if curPath == basePath {
2022-02-03 22:12:58 -05:00
return fmt . Errorf ( "task: include cycle detected between %s <--> %s" ,
2022-01-15 23:34:59 -05:00
curPath ,
2022-08-06 18:19:07 -03:00
filepathext . SmartJoin ( node . Parent . Dir , node . Parent . Entrypoint ) ,
2022-01-15 23:34:59 -05:00
)
}
}
return nil
}