Repository: magefile/mage
Pull request #405 (2022-02-16 22:12:43)

Add magefiles directory support
If no `-d` option is passed to `mage` and there is a folder named `magefolder` then use that as `-d`.

The name was arbitrarily chosen and is up for debate.

I did not add this to the `-init` option because I assumed `-d magefolder` was not such a big pain to do in an infrequent task (unlike running mage targets which happen a lot).

This is a first attempt at addressing https://github.com/magefile/mage/issues/395 and one that is not really invasive but I am willing to grow this into a more complex feature.

I did not consider using non build tagged files as it is somehow useful to have the mage build tag to be able to exclude these. (As I understand the problem mostly arises from mage files living with other package files.)


natefinch: I think it would be beneficial to remove the build tag, since that requires setting it in the language server config. Ideally we'd support it being there or not, so that if you wanted, you could just pop your magefile from your root directory into a subfolder and not need to change anything. 

Re: the name, I think `magefiles` might be better? 
perrito666: > I think it would be beneficial to remove the build tag, since that requires setting it in the language server config. Ideally we'd support it being there or not, so that if you wanted, you could just pop your magefile from your root directory into a subfolder and not need to change anything.

I proposed a change then. This version accepts non-tagged files when inside a `magefiles` directory:

**Caveats**

* If you have a `magefiles` directory we will use it and asume it is for that
* You cannot have mix tagging,if we find tagged and untagged files in a `magefiles` folder, we pick the tagged ones

> 
> Re: the name, I think `magefiles` might be better?

The change also calls this `magefiles` and for good measure swapped `folder` with `directory` in vars and doc so it is consistent with internal usage.

sheldonhull: I'm already storing files in a subfolder. Is the main point of this to eliminate any need for an outer mage.go style file that imports from the subdirectory. That would mean just run mage in the project root and it automatically would pull the magefiles packages?

I currently use:

```
project
- magefile.go
- magefiles/
---- taskpkg1
---- docker
---- kubernetes
```

Would it support this scenario? 
I'm not clear if the go listing command shown is recursive without adding `./...`. 
perrito666: > I'm already storing files in a subfolder. Is the main point of this to eliminate any need for an outer mage.go style file that imports from the subdirectory. That would mean just run mage in the project root and it automatically would pull the magefiles packages?

Indeed, the main motivation here is that many go tools and editors dislike finding two packages in the same directory (as  is often the case when one has a magefile in the main directory along with a different go file). This ends up leading to obscure editor configurations so mage files are treated as go files along with other files.

> 
> I currently use:
> 
> ```
> project
> - magefile.go
> - magefiles/
> ---- taskpkg1
> ---- docker
> ---- kubernetes
> ```
> 
> Would it support this scenario? I'm not clear if the go listing command shown is recursive without adding `./...`.

It is not recursive (I think) but you could put your `magefile.go` inside the `magefiles` folder and that would make all your files available pretty much in the same manner. As a side effect all your mage configuration would be constrained to that directory and subdirectories and your editor of choice could pick it without further configuration.

There is more discussion about how we reached this point [in this issue](https://github.com/magefile/mage/issues/395).
kskewes: This is great. Here are some experimentation results based on the conversation so far.

**IDE support and no build tag**
```
$ find .
.
./magefiles
./magefiles/magefile.go

$ ~/go/bin/mage 
Targets:
  printBase    
```
Neovim LSP works correctly like a regular Go file.

**Finding targets from multiple files without build tag**
```
$ find .
.
./magefiles
./magefiles/magefile.go
./magefiles/3.go
./magefiles/2.go
./magefiles/1.go

$ ~/go/bin/mage 
Targets:
  print1       
  print2       
  print3       
  printBase   
```
> if we find tagged and untagged files in a magefiles folder, we pick the tagged ones

This was experienced as well. Remove tag from all and all found/usable.

**No recursive lookups in `magefiles` directory**
> It is not recursive (I think)

Correct. 
```
$ find .
.
./magefiles
./magefiles/1
./magefiles/1/magefile.go
./magefiles/2
./magefiles/2/magefile.go
./magefiles/3
./magefiles/3/magefile.go

$ ~/go/bin/mage 
No .go files marked with the mage build tag in this directory
```

**`mage -d <dir>` requires build tagged files**
> This version accepts non-tagged files when inside a magefiles directory:

This was experienced.
```
$ find .
.
./magefiles
./magefiles/1
./magefiles/1/magefile.go

$ ~/go/bin/mage -d magefiles/1
No .go files marked with the mage build tag in this directory.

$ sed -i '1 i\//+build mage\n' magefiles/1/magefile.go 

$ ~/go/bin/mage -d magefiles/1
Targets:
  print1  
```



natefinch: I think we're going to update it so that we use build tags in the magefiles directory as per normal. i.e., they're additive.  So, we'll get the list of files by using the "mage" build tag, and just include all files both with and without it, as it works with normal go code.

This way, it won't matter if you used tags or not.  The only thing it'll hinder is having two different packages in the same folder, which is one of the problems we're trying to fix with this change anyway.
perrito666: > I think we're going to update it so that we use build tags in the magefiles directory as per normal. i.e., they're additive. So, we'll get the list of files by using the "mage" build tag, and just include all files both with and without it, as it works with normal go code.
> 
> This way, it won't matter if you used tags or not. The only thing it'll hinder is having two different packages in the same folder, which is one of the problems we're trying to fix with this change anyway.

Done, when mixed tagging is found now it will just use all the files.

Patch:
diff --git a/mage/main.go b/mage/main.go
index f57fdc6..a41e743 100644
--- a/mage/main.go
+++ b/mage/main.go
@@ -118,6 +118,15 @@ type Invocation struct {
 	HashFast   bool          // don't rely on GOCACHE, just hash the magefiles
 }
 
+// MagefilesDirName is the name of the default folder to look for if no directory was specified,
+// if this folder exists it will be assumed mage package lives inside it.
+const MagefilesDirName = "magefiles"
+
+// UsesMagefiles returns true if we are getting our mage files from a magefiles directory.
+func (i Invocation) UsesMagefiles() bool {
+	return i.Dir == MagefilesDirName
+}
+
 // ParseAndRun parses the command line, and then compiles and runs the mage
 // files in the given directory with the given args (do not include the command
 // name in the args).
@@ -180,7 +189,7 @@ func Parse(stderr, stdout io.Writer, args []string) (inv Invocation, cmd Command
 	fs.BoolVar(&inv.Help, "h", false, "show this help")
 	fs.DurationVar(&inv.Timeout, "t", 0, "timeout in duration parsable format (e.g. 5m30s)")
 	fs.BoolVar(&inv.Keep, "keep", false, "keep intermediate mage files around after running")
-	fs.StringVar(&inv.Dir, "d", ".", "directory to read magefiles from")
+	fs.StringVar(&inv.Dir, "d", "", "directory to read magefiles from")
 	fs.StringVar(&inv.WorkDir, "w", "", "working directory where magefiles will run")
 	fs.StringVar(&inv.GoCmd, "gocmd", mg.GoCmd(), "use the given go binary to compile the output")
 	fs.StringVar(&inv.GOOS, "goos", "", "set GOOS for binary produced with -compile")
@@ -216,7 +225,7 @@ Commands:
 
 Options:
   -d <string> 
-            directory to read magefiles from (default ".")
+            directory to read magefiles from (default "." or "magefiles" if exists)
   -debug    turn on debug messages
   -f        force recreation of compiled magefile
   -goarch   sets the GOARCH for the binary created by -compile (default: current arch)
@@ -296,23 +305,50 @@ Options:
 	return inv, cmd, err
 }
 
+const dotDirectory = "."
+
 // Invoke runs Mage with the given arguments.
 func Invoke(inv Invocation) int {
 	errlog := log.New(inv.Stderr, "", 0)
 	if inv.GoCmd == "" {
 		inv.GoCmd = "go"
 	}
+	var noDir bool
 	if inv.Dir == "" {
-		inv.Dir = "."
+		noDir = true
+		inv.Dir = dotDirectory
+		// . will be default unless we find a mage folder.
+		mfSt, err := os.Stat(MagefilesDirName)
+		if err == nil {
+			if mfSt.IsDir() {
+				stderrBuf := &bytes.Buffer{}
+				inv.Dir = MagefilesDirName // preemptive assignment
+				// TODO: Remove this fallback and the above Magefiles invocation when the bw compatibility is removed.
+				files, err := Magefiles(dotDirectory, inv.GOOS, inv.GOARCH, inv.GoCmd, stderrBuf, false, inv.Debug)
+				if err == nil {
+					if len(files) != 0 {
+						errlog.Println("[WARNING] You have both a magefiles directory and mage files in the " +
+							"current directory, in future versions the files will be ignored in favor of the directory")
+						inv.Dir = dotDirectory
+					}
+				}
+			}
+		}
 	}
+
 	if inv.WorkDir == "" {
-		inv.WorkDir = inv.Dir
+		if noDir {
+			inv.WorkDir = dotDirectory
+		} else {
+			inv.WorkDir = inv.Dir
+		}
 	}
+
 	if inv.CacheDir == "" {
 		inv.CacheDir = mg.CacheDir()
 	}
 
-	files, err := Magefiles(inv.Dir, inv.GOOS, inv.GOARCH, inv.GoCmd, inv.Stderr, inv.Debug)
+	files, err := Magefiles(inv.Dir, inv.GOOS, inv.GOARCH, inv.GoCmd, inv.Stderr, inv.UsesMagefiles(), inv.Debug)
 	if err != nil {
 		errlog.Println("Error determining list of magefiles:", err)
 		return 1
@@ -432,69 +468,87 @@ type mainfileTemplateData struct {
 	BinaryName  string
 }
 
+func listGoFiles(magePath, goCmd, tags string, env []string) ([]string, error) {
+	args := []string{"list"}
+	if tags != "" {
+		args = append(args, fmt.Sprintf("-tags=%s", tags))
+	}
+	args = append(args, "-e", "-f", `{{join .GoFiles "||"}}`)
+	cmd := exec.Command(goCmd, args...)
+	cmd.Env = env
+	buf := &bytes.Buffer{}
+	cmd.Stderr = buf
+	cmd.Dir = magePath
+	b, err := cmd.Output()
+	if err != nil {
+		stderr := buf.String()
+		// if the error is "cannot find module", that can mean that there's no
+		// non-mage files, which is fine, so ignore it.
+		if !strings.Contains(stderr, "cannot find module for path") {
+			if tags == "" {
+				return nil, fmt.Errorf("failed to list un-tagged gofiles: %v: %s", err, stderr)
+			}
+			return nil, fmt.Errorf("failed to list gofiles tagged with %q: %v: %s", tags, err, stderr)
+		}
+	}
+	out := strings.TrimSpace(string(b))
+	list := strings.Split(out, "||")
+	for i := range list {
+		list[i] = filepath.Join(magePath, list[i])
+	}
+	return list, nil
+}
+
 // Magefiles returns the list of magefiles in dir.
-func Magefiles(magePath, goos, goarch, goCmd string, stderr io.Writer, isDebug bool) ([]string, error) {
+func Magefiles(magePath, goos, goarch, goCmd string, stderr io.Writer, isMagefilesDirectory, isDebug bool) ([]string, error) {
 	start := time.Now()
 	defer func() {
 		debug.Println("time to scan for Magefiles:", time.Since(start))
 	}()
-	fail := func(err error) ([]string, error) {
-		return nil, err
-	}
 
 	env, err := internal.EnvWithGOOS(goos, goarch)
 	if err != nil {
 		return nil, err
 	}
 
-	debug.Println("getting all non-mage files in", magePath)
+	debug.Println("getting all files including those with mage tag in", magePath)
+	mageFiles, err := listGoFiles(magePath, goCmd, "mage", env)
+	if err != nil {
+		return nil, fmt.Errorf("listing mage files: %v", err)
+	}
 
-	// // first, grab all the files with no build tags specified.. this is actually
-	// // our exclude list of things without the mage build tag.
-	cmd := exec.Command(goCmd, "list", "-e", "-f", `{{join .GoFiles "||"}}`)
-	cmd.Env = env
-	buf := &bytes.Buffer{}
-	cmd.Stderr = buf
-	cmd.Dir = magePath
-	b, err := cmd.Output()
+	if isMagefilesDirectory {
+		// For the magefiles directory, we always use all go files, both with
+		// and without the mage tag, as per normal go build tag rules.
+		debug.Println("using all go files in magefiles directory", magePath)
+		return mageFiles, nil
+	}
+
+	// For folders other than the magefiles directory, we only consider files
+	// that have the mage build tag and ignore those that don't.
+
+	debug.Println("getting all files without mage tag in", magePath)
+	nonMageFiles, err := listGoFiles(magePath, goCmd, "", env)
 	if err != nil {
-		stderr := buf.String()
-		// if the error is "cannot find module", that can mean that there's no
-		// non-mage files, which is fine, so ignore it.
-		if !strings.Contains(stderr, "cannot find module for path") {
-			return fail(fmt.Errorf("failed to list non-mage gofiles: %v: %s", err, stderr))
-		}
+		return nil, fmt.Errorf("listing non-mage files: %v", err)
 	}
-	list := strings.TrimSpace(string(b))
-	debug.Println("found non-mage files", list)
+
+	// convert non-Mage list to a map of files to exclude.
 	exclude := map[string]bool{}
-	for _, f := range strings.Split(list, "||") {
+	for _, f := range nonMageFiles {
 		if f != "" {
 			debug.Printf("marked file as non-mage: %q", f)
 			exclude[f] = true
 		}
 	}
-	debug.Println("getting all files plus mage files")
-	cmd = exec.Command(goCmd, "list", "-tags=mage", "-e", "-f", `{{join .GoFiles "||"}}`)
-	cmd.Env = env
-
-	buf.Reset()
-	cmd.Dir = magePath
-	b, err = cmd.Output()
-	if err != nil {
-		return fail(fmt.Errorf("failed to list mage gofiles: %v: %s", err, buf.Bytes()))
-	}
 
-	list = strings.TrimSpace(string(b))
-	files := []string{}
-	for _, f := range strings.Split(list, "||") {
+	// filter out the non-mage files from the mage files.
+	var files []string
+	for _, f := range mageFiles {
 		if f != "" && !exclude[f] {
 			files = append(files, f)
 		}
 	}
-	for i := range files {
-		files[i] = filepath.Join(magePath, files[i])
-	}
 	return files, nil
 }
 
diff --git a/parse/parse.go b/parse/parse.go
index 37df031..14c5bcd 100644
--- a/parse/parse.go
+++ b/parse/parse.go
@@ -225,8 +225,10 @@ func checkDupes(info *PkgInfo, imports []*Import) error {
 		for _, f := range funcs[d] {
 			ids = append(ids, f.ID())
 		}
+		sort.Strings(ids)
 		errs = append(errs, fmt.Sprintf("%q target has multiple definitions: %s\n", d, strings.Join(ids, ", ")))
 	}
+	sort.Strings(errs)
 	return errors.New(strings.Join(errs, "\n"))
 }
 
diff --git a/site/content/index.md b/site/content/index.md
index 25bfe14..ac0f860 100644
--- a/site/content/index.md
+++ b/site/content/index.md
@@ -89,6 +89,13 @@ func Build() error {
 
 Run the above `Build` target by simply running `mage build` in the same directory as the magefile.
 
+## Magefiles directory
+
+If you create your Magefile or files within a directory named `magefiles` And there is no Magefile in your current directory, 
+`mage` will default to the directory as the source for your targets while keeping the current directory as working one.
+
+The result is the equivalent of running `mage -d magefiles -w .`
+
 ## Demo
 
 {{< youtube Hoga60EF_1U >}}
diff --git a/site/content/magefiles/_index.en.md b/site/content/magefiles/_index.en.md
index 6f90817..dcfb4bd 100644
--- a/site/content/magefiles/_index.en.md
+++ b/site/content/magefiles/_index.en.md
@@ -89,3 +89,17 @@ mage target:
 The first sentence in the comment will be the short help text shown with mage -l.
 The rest of the comment is long help text that will be shown with mage -h <target>
 ```
+
+### Magefiles directory
+
+Some development IDEs might work erratically with magefiles sharing location with other go files which very well might
+have a different package name. 
+
+To simplify development, if you place your Magefiles inside a `magefiles` directory, and none remains in your
+main folder, the `mage` command will pick the directory as the source of your targets while keeping the current directory
+as the working one.
+
+The `magefiles` directory does not require for you to add build tags to the files in it, it is optional, however we 
+encourage you to keep it consistent, either all or none.
+
+Effectively this is the equivalent of running: `mage -d magefiles -w .`
\ No newline at end of file


Test patch:
diff --git a/mage/main_test.go b/mage/main_test.go
index d0b43d8..2074c64 100644
--- a/mage/main_test.go
+++ b/mage/main_test.go
@@ -189,7 +189,7 @@ func TestTransitiveHashFast(t *testing.T) {
 
 func TestListMagefilesMain(t *testing.T) {
 	buf := &bytes.Buffer{}
-	files, err := Magefiles("testdata/mixed_main_files", "", "", "go", buf, false)
+	files, err := Magefiles("testdata/mixed_main_files", "", "", "go", buf, false, false)
 	if err != nil {
 		t.Errorf("error from magefile list: %v: %s", err, buf)
 	}
@@ -210,7 +210,7 @@ func TestListMagefilesIgnoresGOOS(t *testing.T) {
 		os.Setenv("GOOS", "windows")
 	}
 	defer os.Setenv("GOOS", runtime.GOOS)
-	files, err := Magefiles("testdata/goos_magefiles", "", "", "go", buf, false)
+	files, err := Magefiles("testdata/goos_magefiles", "", "", "go", buf, false, false)
 	if err != nil {
 		t.Errorf("error from magefile list: %v: %s", err, buf)
 	}
@@ -234,7 +234,7 @@ func TestListMagefilesIgnoresRespectsGOOSArg(t *testing.T) {
 		goos = "windows"
 	}
 	// Set GOARCH as amd64 because windows is not on all non-x86 architectures.
-	files, err := Magefiles("testdata/goos_magefiles", goos, "amd64", "go", buf, false)
+	files, err := Magefiles("testdata/goos_magefiles", goos, "amd64", "go", buf, false, false)
 	if err != nil {
 		t.Errorf("error from magefile list: %v: %s", err, buf)
 	}
@@ -308,7 +308,7 @@ func TestCompileDiffGoosGoarch(t *testing.T) {
 
 func TestListMagefilesLib(t *testing.T) {
 	buf := &bytes.Buffer{}
-	files, err := Magefiles("testdata/mixed_lib_files", "", "", "go", buf, false)
+	files, err := Magefiles("testdata/mixed_lib_files", "", "", "go", buf, false, false)
 	if err != nil {
 		t.Errorf("error from magefile list: %v: %s", err, buf)
 	}
@@ -342,6 +342,140 @@ func TestMixedMageImports(t *testing.T) {
 	}
 }
 
+func TestMagefilesFolder(t *testing.T) {
+	resetTerm()
+	wd, err := os.Getwd()
+	t.Log(wd)
+	if err != nil {
+		t.Fatalf("finding current working directory: %v", err)
+	}
+	if err := os.Chdir("testdata/with_magefiles_folder"); err != nil {
+		t.Fatalf("changing to magefolders tests data: %v", err)
+	}
+	// restore previous state
+	defer os.Chdir(wd)
+
+	stderr := &bytes.Buffer{}
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "",
+		Stdout: stdout,
+		Stderr: stderr,
+		List:   true,
+	}
+	code := Invoke(inv)
+	if code != 0 {
+		t.Errorf("expected to exit with code 0, but got %v, stderr: %s", code, stderr)
+	}
+	expected := "Targets:\n  build    \n"
+	actual := stdout.String()
+	if actual != expected {
+		t.Fatalf("expected %q but got %q", expected, actual)
+	}
+}
+
+func TestMagefilesFolderMixedWithMagefiles(t *testing.T) {
+	resetTerm()
+	wd, err := os.Getwd()
+	t.Log(wd)
+	if err != nil {
+		t.Fatalf("finding current working directory: %v", err)
+	}
+	if err := os.Chdir("testdata/with_magefiles_folder_and_mage_files_in_dot"); err != nil {
+		t.Fatalf("changing to magefolders tests data: %v", err)
+	}
+	// restore previous state
+	defer os.Chdir(wd)
+
+	stderr := &bytes.Buffer{}
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "",
+		Stdout: stdout,
+		Stderr: stderr,
+		List:   true,
+	}
+	code := Invoke(inv)
+	if code != 0 {
+		t.Errorf("expected to exit with code 0, but got %v, stderr: %s", code, stderr)
+	}
+	expected := "Targets:\n  build    \n"
+	actual := stdout.String()
+	if actual != expected {
+		t.Fatalf("expected %q but got %q", expected, actual)
+	}
+
+	expectedErr := "[WARNING] You have both a magefiles directory and mage files in the current directory, in future versions the files will be ignored in favor of the directory\n"
+	actualErr := stderr.String()
+	if actualErr != expectedErr {
+		t.Fatalf("expected Warning %q but got %q", expectedErr, actualErr)
+	}
+}
+
+func TestUntaggedMagefilesFolder(t *testing.T) {
+	resetTerm()
+	wd, err := os.Getwd()
+	t.Log(wd)
+	if err != nil {
+		t.Fatalf("finding current working directory: %v", err)
+	}
+	if err := os.Chdir("testdata/with_untagged_magefiles_folder"); err != nil {
+		t.Fatalf("changing to magefolders tests data: %v", err)
+	}
+	// restore previous state
+	defer os.Chdir(wd)
+
+	stderr := &bytes.Buffer{}
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "",
+		Stdout: stdout,
+		Stderr: stderr,
+		List:   true,
+	}
+	code := Invoke(inv)
+	if code != 0 {
+		t.Errorf("expected to exit with code 0, but got %v, stderr: %s", code, stderr)
+	}
+	expected := "Targets:\n  build    \n"
+	actual := stdout.String()
+	if actual != expected {
+		t.Fatalf("expected %q but got %q", expected, actual)
+	}
+}
+
+func TestMixedTaggingMagefilesFolder(t *testing.T) {
+	resetTerm()
+	wd, err := os.Getwd()
+	t.Log(wd)
+	if err != nil {
+		t.Fatalf("finding current working directory: %v", err)
+	}
+	if err := os.Chdir("testdata/with_mixtagged_magefiles_folder"); err != nil {
+		t.Fatalf("changing to magefolders tests data: %v", err)
+	}
+	// restore previous state
+	defer os.Chdir(wd)
+
+	stderr := &bytes.Buffer{}
+	stdout := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "",
+		Stdout: stdout,
+		Stderr: stderr,
+		List:   true,
+	}
+	code := Invoke(inv)
+	if code != 0 {
+		t.Errorf("expected to exit with code 0, but got %v, stderr: %s", code, stderr)
+	}
+	expected := "Targets:\n  build            \n  untaggedBuild    \n"
+	actual := stdout.String()
+	if actual != expected {
+		t.Fatalf("expected %q but got %q", expected, actual)
+	}
+}
+
 func TestGoRun(t *testing.T) {
 	c := exec.Command("go", "run", "main.go")
 	c.Dir = "./testdata"
@@ -1615,7 +1749,7 @@ func TestWrongDependency(t *testing.T) {
 	}
 }
 
-/// This code liberally borrowed from https://github.com/rsc/goversion/blob/master/version/exe.go
+// / This code liberally borrowed from https://github.com/rsc/goversion/blob/master/version/exe.go
 
 type (
 	exeType  int
diff --git a/mage/testdata/with_magefiles_folder/magefiles/mage_helpers.go b/mage/testdata/with_magefiles_folder/magefiles/mage_helpers.go
new file mode 100644
index 0000000..e4b8872
--- /dev/null
+++ b/mage/testdata/with_magefiles_folder/magefiles/mage_helpers.go
@@ -0,0 +1,6 @@
+//go:build mage
+// +build mage
+
+package main
+
+func foo() {}
diff --git a/mage/testdata/with_magefiles_folder/magefiles/magefile.go b/mage/testdata/with_magefiles_folder/magefiles/magefile.go
new file mode 100644
index 0000000..6358450
--- /dev/null
+++ b/mage/testdata/with_magefiles_folder/magefiles/magefile.go
@@ -0,0 +1,6 @@
+//go:build mage
+// +build mage
+
+package main
+
+func Build() {}
diff --git a/mage/testdata/with_magefiles_folder_and_mage_files_in_dot/magefile.go b/mage/testdata/with_magefiles_folder_and_mage_files_in_dot/magefile.go
new file mode 100644
index 0000000..6358450
--- /dev/null
+++ b/mage/testdata/with_magefiles_folder_and_mage_files_in_dot/magefile.go
@@ -0,0 +1,6 @@
+//go:build mage
+// +build mage
+
+package main
+
+func Build() {}
diff --git a/mage/testdata/with_magefiles_folder_and_mage_files_in_dot/magefiles/mage_helpers.go b/mage/testdata/with_magefiles_folder_and_mage_files_in_dot/magefiles/mage_helpers.go
new file mode 100644
index 0000000..e4b8872
--- /dev/null
+++ b/mage/testdata/with_magefiles_folder_and_mage_files_in_dot/magefiles/mage_helpers.go
@@ -0,0 +1,6 @@
+//go:build mage
+// +build mage
+
+package main
+
+func foo() {}
diff --git a/mage/testdata/with_magefiles_folder_and_mage_files_in_dot/magefiles/magefile.go b/mage/testdata/with_magefiles_folder_and_mage_files_in_dot/magefiles/magefile.go
new file mode 100644
index 0000000..a465aaf
--- /dev/null
+++ b/mage/testdata/with_magefiles_folder_and_mage_files_in_dot/magefiles/magefile.go
@@ -0,0 +1,6 @@
+//go:build mage
+// +build mage
+
+package main
+
+func FolderBuild() {}
diff --git a/mage/testdata/with_mixtagged_magefiles_folder/magefiles/mage_helpers.go b/mage/testdata/with_mixtagged_magefiles_folder/magefiles/mage_helpers.go
new file mode 100644
index 0000000..e4b8872
--- /dev/null
+++ b/mage/testdata/with_mixtagged_magefiles_folder/magefiles/mage_helpers.go
@@ -0,0 +1,6 @@
+//go:build mage
+// +build mage
+
+package main
+
+func foo() {}
diff --git a/mage/testdata/with_mixtagged_magefiles_folder/magefiles/magefile.go b/mage/testdata/with_mixtagged_magefiles_folder/magefiles/magefile.go
new file mode 100644
index 0000000..6358450
--- /dev/null
+++ b/mage/testdata/with_mixtagged_magefiles_folder/magefiles/magefile.go
@@ -0,0 +1,6 @@
+//go:build mage
+// +build mage
+
+package main
+
+func Build() {}
diff --git a/mage/testdata/with_mixtagged_magefiles_folder/magefiles/main.go b/mage/testdata/with_mixtagged_magefiles_folder/magefiles/main.go
new file mode 100644
index 0000000..4c65d86
--- /dev/null
+++ b/mage/testdata/with_mixtagged_magefiles_folder/magefiles/main.go
@@ -0,0 +1,7 @@
+// some points here: this is the main pacakge just like the magefiles.  This one
+// has a main function, which could conflict with the generated main function we
+// make (but clearly shouldn't cause problems). Finally, there's a duplicate function name here.
+
+package main
+
+func UntaggedBuild() {}
diff --git a/mage/testdata/with_untagged_magefiles_folder/magefiles/mage_helpers.go b/mage/testdata/with_untagged_magefiles_folder/magefiles/mage_helpers.go
new file mode 100644
index 0000000..3de84a3
--- /dev/null
+++ b/mage/testdata/with_untagged_magefiles_folder/magefiles/mage_helpers.go
@@ -0,0 +1,3 @@
+package main
+
+func foo() {}
diff --git a/mage/testdata/with_untagged_magefiles_folder/magefiles/magefile.go b/mage/testdata/with_untagged_magefiles_folder/magefiles/magefile.go
new file mode 100644
index 0000000..5660fe1
--- /dev/null
+++ b/mage/testdata/with_untagged_magefiles_folder/magefiles/magefile.go
@@ -0,0 +1,3 @@
+package main
+
+func Build() {}