mirror of
https://github.com/go-task/task.git
synced 2025-04-19 12:12:27 +02:00
Resolve relative include paths relative to the including Taskfile
Closes #823 Closes #822
This commit is contained in:
parent
47c1bb6a5b
commit
e396f4d06f
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Always resolve relative include paths relative to the including Taskfile
|
||||||
|
([#822](https://github.com/go-task/task/issues/822), [#823](https://github.com/go-task/task/pull/823)).
|
||||||
- Fix ZSH and PowerShell completions to consider all tasks instead of just the
|
- Fix ZSH and PowerShell completions to consider all tasks instead of just the
|
||||||
public ones (those with descriptions)
|
public ones (those with descriptions)
|
||||||
([#803](https://github.com/go-task/task/pull/803)).
|
([#803](https://github.com/go-task/task/pull/803)).
|
||||||
|
@ -81,7 +81,7 @@ Some environment variables can be overriden to adjust Task behavior.
|
|||||||
|
|
||||||
| Attribute | Type | Default | Description |
|
| Attribute | Type | Default | Description |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
| `taskfile` | `string` | | The path for the Taskfile or directory to be included. If a directory, Task will look for files named `Taskfile.yml` or `Taskfile.yaml` inside that directory. |
|
| `taskfile` | `string` | | The path for the Taskfile or directory to be included. If a directory, Task will look for files named `Taskfile.yml` or `Taskfile.yaml` inside that directory. If a relative path, resolved relative to the directory containing the including Taskfile. |
|
||||||
| `dir` | `string` | The parent Taskfile directory | The working directory of the included tasks when run. |
|
| `dir` | `string` | The parent Taskfile directory | The working directory of the included tasks when run. |
|
||||||
| `optional` | `bool` | `false` | If `true`, no errors will be thrown if the specified file does not exist. |
|
| `optional` | `bool` | `false` | If `true`, no errors will be thrown if the specified file does not exist. |
|
||||||
|
|
||||||
|
@ -136,6 +136,8 @@ namespace. So, you'd call `task docs:serve` to run the `serve` task from
|
|||||||
`documentation/Taskfile.yml` or `task docker:build` to run the `build` task
|
`documentation/Taskfile.yml` or `task docker:build` to run the `build` task
|
||||||
from the `DockerTasks.yml` file.
|
from the `DockerTasks.yml` file.
|
||||||
|
|
||||||
|
Relative paths are resolved relative to the directory containing the including Taskfile.
|
||||||
|
|
||||||
### OS-specific Taskfiles
|
### OS-specific Taskfiles
|
||||||
|
|
||||||
With `version: '2'`, task automatically includes any `Taskfile_{{OS}}.yml`
|
With `version: '2'`, task automatically includes any `Taskfile_{{OS}}.yml`
|
||||||
|
52
task_test.go
52
task_test.go
@ -52,13 +52,14 @@ func (fct fileContentTest) Run(t *testing.T) {
|
|||||||
|
|
||||||
for name, expectContent := range fct.Files {
|
for name, expectContent := range fct.Files {
|
||||||
t.Run(fct.name(name), func(t *testing.T) {
|
t.Run(fct.name(name), func(t *testing.T) {
|
||||||
b, err := os.ReadFile(filepath.Join(fct.Dir, name))
|
path := filepath.Join(fct.Dir, name)
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
assert.NoError(t, err, "Error reading file")
|
assert.NoError(t, err, "Error reading file")
|
||||||
s := string(b)
|
s := string(b)
|
||||||
if fct.TrimSpace {
|
if fct.TrimSpace {
|
||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
}
|
}
|
||||||
assert.Equal(t, expectContent, s, "unexpected file content")
|
assert.Equal(t, expectContent, s, "unexpected file content in %s", path)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -774,7 +775,12 @@ func TestIncludesMultiLevel(t *testing.T) {
|
|||||||
|
|
||||||
func TestIncludeCycle(t *testing.T) {
|
func TestIncludeCycle(t *testing.T) {
|
||||||
const dir = "testdata/includes_cycle"
|
const dir = "testdata/includes_cycle"
|
||||||
expectedError := "task: include cycle detected between testdata/includes_cycle/Taskfile.yml <--> testdata/includes_cycle/one/two/Taskfile.yml"
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
message := "task: include cycle detected between %s/%s/one/Taskfile.yml <--> %s/%s/Taskfile.yml"
|
||||||
|
expectedError := fmt.Sprintf(message, wd, dir, wd, dir)
|
||||||
|
|
||||||
var buff bytes.Buffer
|
var buff bytes.Buffer
|
||||||
e := task.Executor{
|
e := task.Executor{
|
||||||
@ -852,27 +858,39 @@ func TestIncludesOptional(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIncludesOptionalImplicitFalse(t *testing.T) {
|
func TestIncludesOptionalImplicitFalse(t *testing.T) {
|
||||||
|
const dir = "testdata/includes_optional_implicit_false"
|
||||||
|
wd, _ := os.Getwd()
|
||||||
|
|
||||||
|
message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
|
||||||
|
expected := fmt.Sprintf(message, wd, dir)
|
||||||
|
|
||||||
e := task.Executor{
|
e := task.Executor{
|
||||||
Dir: "testdata/includes_optional_implicit_false",
|
Dir: dir,
|
||||||
Stdout: io.Discard,
|
Stdout: io.Discard,
|
||||||
Stderr: io.Discard,
|
Stderr: io.Discard,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := e.Setup()
|
err := e.Setup()
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, "stat testdata/includes_optional_implicit_false/TaskfileOptional.yml: no such file or directory", err.Error())
|
assert.Equal(t, expected, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIncludesOptionalExplicitFalse(t *testing.T) {
|
func TestIncludesOptionalExplicitFalse(t *testing.T) {
|
||||||
|
const dir = "testdata/includes_optional_explicit_false"
|
||||||
|
wd, _ := os.Getwd()
|
||||||
|
|
||||||
|
message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
|
||||||
|
expected := fmt.Sprintf(message, wd, dir)
|
||||||
|
|
||||||
e := task.Executor{
|
e := task.Executor{
|
||||||
Dir: "testdata/includes_optional_explicit_false",
|
Dir: dir,
|
||||||
Stdout: io.Discard,
|
Stdout: io.Discard,
|
||||||
Stderr: io.Discard,
|
Stderr: io.Discard,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := e.Setup()
|
err := e.Setup()
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, "stat testdata/includes_optional_explicit_false/TaskfileOptional.yml: no such file or directory", err.Error())
|
assert.Equal(t, expected, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIncludesFromCustomTaskfile(t *testing.T) {
|
func TestIncludesFromCustomTaskfile(t *testing.T) {
|
||||||
@ -890,6 +908,26 @@ func TestIncludesFromCustomTaskfile(t *testing.T) {
|
|||||||
tt.Run(t)
|
tt.Run(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIncludesRelativePath(t *testing.T) {
|
||||||
|
const dir = "testdata/includes_rel_path"
|
||||||
|
|
||||||
|
var buff bytes.Buffer
|
||||||
|
e := task.Executor{
|
||||||
|
Dir: dir,
|
||||||
|
Stdout: &buff,
|
||||||
|
Stderr: &buff,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, e.Setup())
|
||||||
|
|
||||||
|
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "common:pwd"}))
|
||||||
|
assert.Contains(t, buff.String(), "testdata/includes_rel_path/common")
|
||||||
|
|
||||||
|
buff.Reset()
|
||||||
|
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "included:common:pwd"}))
|
||||||
|
assert.Contains(t, buff.String(), "testdata/includes_rel_path/common")
|
||||||
|
}
|
||||||
|
|
||||||
func TestSupportedFileNames(t *testing.T) {
|
func TestSupportedFileNames(t *testing.T) {
|
||||||
fileNames := []string{
|
fileNames := []string{
|
||||||
"Taskfile.yml",
|
"Taskfile.yml",
|
||||||
|
@ -2,17 +2,22 @@ package taskfile
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/go-task/task/v3/internal/execext"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IncludedTaskfile represents information about included tasksfile
|
// IncludedTaskfile represents information about included taskfiles
|
||||||
type IncludedTaskfile struct {
|
type IncludedTaskfile struct {
|
||||||
Taskfile string
|
Taskfile string
|
||||||
Dir string
|
Dir string
|
||||||
Optional bool
|
Optional bool
|
||||||
AdvancedImport bool
|
AdvancedImport bool
|
||||||
Vars *Vars
|
Vars *Vars
|
||||||
|
BaseDir string // The directory from which the including taskfile was loaded; used to resolve relative paths
|
||||||
}
|
}
|
||||||
|
|
||||||
// IncludedTaskfiles represents information about included tasksfiles
|
// IncludedTaskfiles represents information about included tasksfiles
|
||||||
@ -107,3 +112,31 @@ func (it *IncludedTaskfile) UnmarshalYAML(unmarshal func(interface{}) error) err
|
|||||||
it.Vars = includedTaskfile.Vars
|
it.Vars = includedTaskfile.Vars
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FullTaskfilePath returns the fully qualified path to the included taskfile
|
||||||
|
func (it *IncludedTaskfile) FullTaskfilePath() (string, error) {
|
||||||
|
return it.resolvePath(it.Taskfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FullDirPath returns the fully qualified path to the included taskfile's working directory
|
||||||
|
func (it *IncludedTaskfile) FullDirPath() (string, error) {
|
||||||
|
return it.resolvePath(it.Dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *IncludedTaskfile) resolvePath(path string) (string, error) {
|
||||||
|
path, err := execext.Expand(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if filepath.IsAbs(path) {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := filepath.Abs(filepath.Join(it.BaseDir, path))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("task: error resolving path %s relative to %s: %w", path, it.BaseDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/go-task/task/v3/internal/execext"
|
|
||||||
"github.com/go-task/task/v3/internal/templater"
|
"github.com/go-task/task/v3/internal/templater"
|
||||||
"github.com/go-task/task/v3/taskfile"
|
"github.com/go-task/task/v3/taskfile"
|
||||||
)
|
)
|
||||||
@ -44,6 +43,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
|
|||||||
}
|
}
|
||||||
readerNode.Dir = d
|
readerNode.Dir = d
|
||||||
}
|
}
|
||||||
|
|
||||||
path, err := exists(filepath.Join(readerNode.Dir, readerNode.Entrypoint))
|
path, err := exists(filepath.Join(readerNode.Dir, readerNode.Entrypoint))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -60,6 +60,16 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
})
|
||||||
|
|
||||||
err = t.Includes.Range(func(namespace string, includedTask taskfile.IncludedTaskfile) error {
|
err = t.Includes.Range(func(namespace string, includedTask taskfile.IncludedTaskfile) error {
|
||||||
if v >= 3.0 {
|
if v >= 3.0 {
|
||||||
tr := templater.Templater{Vars: &taskfile.Vars{}, RemoveNoValue: true}
|
tr := templater.Templater{Vars: &taskfile.Vars{}, RemoveNoValue: true}
|
||||||
@ -69,19 +79,18 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
|
|||||||
Optional: includedTask.Optional,
|
Optional: includedTask.Optional,
|
||||||
AdvancedImport: includedTask.AdvancedImport,
|
AdvancedImport: includedTask.AdvancedImport,
|
||||||
Vars: includedTask.Vars,
|
Vars: includedTask.Vars,
|
||||||
|
BaseDir: includedTask.BaseDir,
|
||||||
}
|
}
|
||||||
if err := tr.Err(); err != nil {
|
if err := tr.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
path, err := execext.Expand(includedTask.Taskfile)
|
path, err := includedTask.FullTaskfilePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !filepath.IsAbs(path) {
|
|
||||||
path = filepath.Join(readerNode.Dir, path)
|
|
||||||
}
|
|
||||||
path, err = exists(path)
|
path, err = exists(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if includedTask.Optional {
|
if includedTask.Optional {
|
||||||
@ -114,21 +123,27 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if includedTask.AdvancedImport {
|
if includedTask.AdvancedImport {
|
||||||
|
dir, err := includedTask.FullDirPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for k, v := range includedTaskfile.Vars.Mapping {
|
for k, v := range includedTaskfile.Vars.Mapping {
|
||||||
o := v
|
o := v
|
||||||
o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
|
o.Dir = dir
|
||||||
includedTaskfile.Vars.Mapping[k] = o
|
includedTaskfile.Vars.Mapping[k] = o
|
||||||
}
|
}
|
||||||
for k, v := range includedTaskfile.Env.Mapping {
|
for k, v := range includedTaskfile.Env.Mapping {
|
||||||
o := v
|
o := v
|
||||||
o.Dir = filepath.Join(readerNode.Dir, includedTask.Dir)
|
o.Dir = dir
|
||||||
includedTaskfile.Env.Mapping[k] = o
|
includedTaskfile.Env.Mapping[k] = o
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, task := range includedTaskfile.Tasks {
|
for _, task := range includedTaskfile.Tasks {
|
||||||
if !filepath.IsAbs(task.Dir) {
|
if !filepath.IsAbs(task.Dir) {
|
||||||
task.Dir = filepath.Join(includedTask.Dir, task.Dir)
|
task.Dir = filepath.Join(dir, task.Dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
task.IncludeVars = includedTask.Vars
|
task.IncludeVars = includedTask.Vars
|
||||||
task.IncludedTaskfileVars = includedTaskfile.Vars
|
task.IncludedTaskfileVars = includedTaskfile.Vars
|
||||||
}
|
}
|
||||||
@ -176,19 +191,29 @@ func readTaskfile(file string) (*taskfile.Taskfile, error) {
|
|||||||
return &t, yaml.NewDecoder(f).Decode(&t)
|
return &t, yaml.NewDecoder(f).Decode(&t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exists finds a Taskfile at the stated location, returning a fully qualified path to the file
|
||||||
func exists(path string) (string, error) {
|
func exists(path string) (string, error) {
|
||||||
fi, err := os.Stat(path)
|
fi, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if fi.Mode().IsRegular() {
|
if fi.Mode().IsRegular() {
|
||||||
return path, nil
|
// File exists, return a fully qualified path
|
||||||
|
result, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, n := range defaultTaskfiles {
|
for _, n := range defaultTaskfiles {
|
||||||
fpath := filepath.Join(path, n)
|
fpath := filepath.Join(path, n)
|
||||||
if _, err := os.Stat(fpath); err == nil {
|
if _, err := os.Stat(fpath); err == nil {
|
||||||
return fpath, nil
|
result, err := filepath.Abs(fpath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
10
testdata/includes_rel_path/Taskfile.yml
vendored
Normal file
10
testdata/includes_rel_path/Taskfile.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
included:
|
||||||
|
taskfile: ./included
|
||||||
|
dir: ./included
|
||||||
|
|
||||||
|
common:
|
||||||
|
taskfile: ./common
|
||||||
|
dir: ./common
|
4
testdata/includes_rel_path/common/Taskfile.yml
vendored
Normal file
4
testdata/includes_rel_path/common/Taskfile.yml
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
pwd: pwd
|
6
testdata/includes_rel_path/included/Taskfile.yml
vendored
Normal file
6
testdata/includes_rel_path/included/Taskfile.yml
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common:
|
||||||
|
taskfile: ../common
|
||||||
|
dir: ../common
|
Loading…
x
Reference in New Issue
Block a user