Skip to content

Commit 3504e09

Browse files
Add magefiles directory support (#405)
* Use magefolder if no directory set and it exists. If no directory was passed by the user as an explicit option and there is a folder named "magefolder" use that. Workdir is kept as it is likely still "." * Remove the default . for -d flag Also correct os.Stat error checking to expect no error * Add tests and test data for magefolder * Rename magefolder and accept untagged files Magefolder was renamed to magefiles We now accept files that are not tagged too when using a magefiles directory * Assume tagging when mix tagging is present When using magefiles directory, if there are mixed tagging files assume tagging is used for mage files * Update error format to %v We support building for older go versions so error formatting should use %v * sort outputs * Accept mixed tagging in magefiles folder When mixed tagging is found within a magefiles folder, opt to use all files * little tweak to only do go list once when using magefiles directory * Add magefiles directory information to the website * Add a preference for mage files over directories Add a temporary preference for mage files over magefiles directories and warn users this is a temporary functionality leading to a change where directory will be preferred. Co-authored-by: Nate Finch <natefinch@github.com>
1 parent 526bf47 commit 3504e09

File tree

15 files changed

+314
-48
lines changed

15 files changed

+314
-48
lines changed

mage/main.go

Lines changed: 97 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@ type Invocation struct {
118118
HashFast bool // don't rely on GOCACHE, just hash the magefiles
119119
}
120120

121+
// MagefilesDirName is the name of the default folder to look for if no directory was specified,
122+
// if this folder exists it will be assumed mage package lives inside it.
123+
const MagefilesDirName = "magefiles"
124+
125+
// UsesMagefiles returns true if we are getting our mage files from a magefiles directory.
126+
func (i Invocation) UsesMagefiles() bool {
127+
return i.Dir == MagefilesDirName
128+
}
129+
121130
// ParseAndRun parses the command line, and then compiles and runs the mage
122131
// files in the given directory with the given args (do not include the command
123132
// name in the args).
@@ -180,7 +189,7 @@ func Parse(stderr, stdout io.Writer, args []string) (inv Invocation, cmd Command
180189
fs.BoolVar(&inv.Help, "h", false, "show this help")
181190
fs.DurationVar(&inv.Timeout, "t", 0, "timeout in duration parsable format (e.g. 5m30s)")
182191
fs.BoolVar(&inv.Keep, "keep", false, "keep intermediate mage files around after running")
183-
fs.StringVar(&inv.Dir, "d", ".", "directory to read magefiles from")
192+
fs.StringVar(&inv.Dir, "d", "", "directory to read magefiles from")
184193
fs.StringVar(&inv.WorkDir, "w", "", "working directory where magefiles will run")
185194
fs.StringVar(&inv.GoCmd, "gocmd", mg.GoCmd(), "use the given go binary to compile the output")
186195
fs.StringVar(&inv.GOOS, "goos", "", "set GOOS for binary produced with -compile")
@@ -216,7 +225,7 @@ Commands:
216225
217226
Options:
218227
-d <string>
219-
directory to read magefiles from (default ".")
228+
directory to read magefiles from (default "." or "magefiles" if exists)
220229
-debug turn on debug messages
221230
-f force recreation of compiled magefile
222231
-goarch sets the GOARCH for the binary created by -compile (default: current arch)
@@ -296,23 +305,50 @@ Options:
296305
return inv, cmd, err
297306
}
298307

308+
const dotDirectory = "."
309+
299310
// Invoke runs Mage with the given arguments.
300311
func Invoke(inv Invocation) int {
301312
errlog := log.New(inv.Stderr, "", 0)
302313
if inv.GoCmd == "" {
303314
inv.GoCmd = "go"
304315
}
316+
var noDir bool
305317
if inv.Dir == "" {
306-
inv.Dir = "."
318+
noDir = true
319+
inv.Dir = dotDirectory
320+
// . will be default unless we find a mage folder.
321+
mfSt, err := os.Stat(MagefilesDirName)
322+
if err == nil {
323+
if mfSt.IsDir() {
324+
stderrBuf := &bytes.Buffer{}
325+
inv.Dir = MagefilesDirName // preemptive assignment
326+
// TODO: Remove this fallback and the above Magefiles invocation when the bw compatibility is removed.
327+
files, err := Magefiles(dotDirectory, inv.GOOS, inv.GOARCH, inv.GoCmd, stderrBuf, false, inv.Debug)
328+
if err == nil {
329+
if len(files) != 0 {
330+
errlog.Println("[WARNING] You have both a magefiles directory and mage files in the " +
331+
"current directory, in future versions the files will be ignored in favor of the directory")
332+
inv.Dir = dotDirectory
333+
}
334+
}
335+
}
336+
}
307337
}
338+
308339
if inv.WorkDir == "" {
309-
inv.WorkDir = inv.Dir
340+
if noDir {
341+
inv.WorkDir = dotDirectory
342+
} else {
343+
inv.WorkDir = inv.Dir
344+
}
310345
}
346+
311347
if inv.CacheDir == "" {
312348
inv.CacheDir = mg.CacheDir()
313349
}
314350

315-
files, err := Magefiles(inv.Dir, inv.GOOS, inv.GOARCH, inv.GoCmd, inv.Stderr, inv.Debug)
351+
files, err := Magefiles(inv.Dir, inv.GOOS, inv.GOARCH, inv.GoCmd, inv.Stderr, inv.UsesMagefiles(), inv.Debug)
316352
if err != nil {
317353
errlog.Println("Error determining list of magefiles:", err)
318354
return 1
@@ -432,69 +468,87 @@ type mainfileTemplateData struct {
432468
BinaryName string
433469
}
434470

471+
func listGoFiles(magePath, goCmd, tags string, env []string) ([]string, error) {
472+
args := []string{"list"}
473+
if tags != "" {
474+
args = append(args, fmt.Sprintf("-tags=%s", tags))
475+
}
476+
args = append(args, "-e", "-f", `{{join .GoFiles "||"}}`)
477+
cmd := exec.Command(goCmd, args...)
478+
cmd.Env = env
479+
buf := &bytes.Buffer{}
480+
cmd.Stderr = buf
481+
cmd.Dir = magePath
482+
b, err := cmd.Output()
483+
if err != nil {
484+
stderr := buf.String()
485+
// if the error is "cannot find module", that can mean that there's no
486+
// non-mage files, which is fine, so ignore it.
487+
if !strings.Contains(stderr, "cannot find module for path") {
488+
if tags == "" {
489+
return nil, fmt.Errorf("failed to list un-tagged gofiles: %v: %s", err, stderr)
490+
}
491+
return nil, fmt.Errorf("failed to list gofiles tagged with %q: %v: %s", tags, err, stderr)
492+
}
493+
}
494+
out := strings.TrimSpace(string(b))
495+
list := strings.Split(out, "||")
496+
for i := range list {
497+
list[i] = filepath.Join(magePath, list[i])
498+
}
499+
return list, nil
500+
}
501+
435502
// Magefiles returns the list of magefiles in dir.
436-
func Magefiles(magePath, goos, goarch, goCmd string, stderr io.Writer, isDebug bool) ([]string, error) {
503+
func Magefiles(magePath, goos, goarch, goCmd string, stderr io.Writer, isMagefilesDirectory, isDebug bool) ([]string, error) {
437504
start := time.Now()
438505
defer func() {
439506
debug.Println("time to scan for Magefiles:", time.Since(start))
440507
}()
441-
fail := func(err error) ([]string, error) {
442-
return nil, err
443-
}
444508

445509
env, err := internal.EnvWithGOOS(goos, goarch)
446510
if err != nil {
447511
return nil, err
448512
}
449513

450-
debug.Println("getting all non-mage files in", magePath)
514+
debug.Println("getting all files including those with mage tag in", magePath)
515+
mageFiles, err := listGoFiles(magePath, goCmd, "mage", env)
516+
if err != nil {
517+
return nil, fmt.Errorf("listing mage files: %v", err)
518+
}
451519

452-
// // first, grab all the files with no build tags specified.. this is actually
453-
// // our exclude list of things without the mage build tag.
454-
cmd := exec.Command(goCmd, "list", "-e", "-f", `{{join .GoFiles "||"}}`)
455-
cmd.Env = env
456-
buf := &bytes.Buffer{}
457-
cmd.Stderr = buf
458-
cmd.Dir = magePath
459-
b, err := cmd.Output()
520+
if isMagefilesDirectory {
521+
// For the magefiles directory, we always use all go files, both with
522+
// and without the mage tag, as per normal go build tag rules.
523+
debug.Println("using all go files in magefiles directory", magePath)
524+
return mageFiles, nil
525+
}
526+
527+
// For folders other than the magefiles directory, we only consider files
528+
// that have the mage build tag and ignore those that don't.
529+
530+
debug.Println("getting all files without mage tag in", magePath)
531+
nonMageFiles, err := listGoFiles(magePath, goCmd, "", env)
460532
if err != nil {
461-
stderr := buf.String()
462-
// if the error is "cannot find module", that can mean that there's no
463-
// non-mage files, which is fine, so ignore it.
464-
if !strings.Contains(stderr, "cannot find module for path") {
465-
return fail(fmt.Errorf("failed to list non-mage gofiles: %v: %s", err, stderr))
466-
}
533+
return nil, fmt.Errorf("listing non-mage files: %v", err)
467534
}
468-
list := strings.TrimSpace(string(b))
469-
debug.Println("found non-mage files", list)
535+
536+
// convert non-Mage list to a map of files to exclude.
470537
exclude := map[string]bool{}
471-
for _, f := range strings.Split(list, "||") {
538+
for _, f := range nonMageFiles {
472539
if f != "" {
473540
debug.Printf("marked file as non-mage: %q", f)
474541
exclude[f] = true
475542
}
476543
}
477-
debug.Println("getting all files plus mage files")
478-
cmd = exec.Command(goCmd, "list", "-tags=mage", "-e", "-f", `{{join .GoFiles "||"}}`)
479-
cmd.Env = env
480-
481-
buf.Reset()
482-
cmd.Dir = magePath
483-
b, err = cmd.Output()
484-
if err != nil {
485-
return fail(fmt.Errorf("failed to list mage gofiles: %v: %s", err, buf.Bytes()))
486-
}
487544

488-
list = strings.TrimSpace(string(b))
489-
files := []string{}
490-
for _, f := range strings.Split(list, "||") {
545+
// filter out the non-mage files from the mage files.
546+
var files []string
547+
for _, f := range mageFiles {
491548
if f != "" && !exclude[f] {
492549
files = append(files, f)
493550
}
494551
}
495-
for i := range files {
496-
files[i] = filepath.Join(magePath, files[i])
497-
}
498552
return files, nil
499553
}
500554

mage/main_test.go

Lines changed: 139 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ func TestTransitiveHashFast(t *testing.T) {
189189

190190
func TestListMagefilesMain(t *testing.T) {
191191
buf := &bytes.Buffer{}
192-
files, err := Magefiles("testdata/mixed_main_files", "", "", "go", buf, false)
192+
files, err := Magefiles("testdata/mixed_main_files", "", "", "go", buf, false, false)
193193
if err != nil {
194194
t.Errorf("error from magefile list: %v: %s", err, buf)
195195
}
@@ -210,7 +210,7 @@ func TestListMagefilesIgnoresGOOS(t *testing.T) {
210210
os.Setenv("GOOS", "windows")
211211
}
212212
defer os.Setenv("GOOS", runtime.GOOS)
213-
files, err := Magefiles("testdata/goos_magefiles", "", "", "go", buf, false)
213+
files, err := Magefiles("testdata/goos_magefiles", "", "", "go", buf, false, false)
214214
if err != nil {
215215
t.Errorf("error from magefile list: %v: %s", err, buf)
216216
}
@@ -234,7 +234,7 @@ func TestListMagefilesIgnoresRespectsGOOSArg(t *testing.T) {
234234
goos = "windows"
235235
}
236236
// Set GOARCH as amd64 because windows is not on all non-x86 architectures.
237-
files, err := Magefiles("testdata/goos_magefiles", goos, "amd64", "go", buf, false)
237+
files, err := Magefiles("testdata/goos_magefiles", goos, "amd64", "go", buf, false, false)
238238
if err != nil {
239239
t.Errorf("error from magefile list: %v: %s", err, buf)
240240
}
@@ -308,7 +308,7 @@ func TestCompileDiffGoosGoarch(t *testing.T) {
308308

309309
func TestListMagefilesLib(t *testing.T) {
310310
buf := &bytes.Buffer{}
311-
files, err := Magefiles("testdata/mixed_lib_files", "", "", "go", buf, false)
311+
files, err := Magefiles("testdata/mixed_lib_files", "", "", "go", buf, false, false)
312312
if err != nil {
313313
t.Errorf("error from magefile list: %v: %s", err, buf)
314314
}
@@ -342,6 +342,140 @@ func TestMixedMageImports(t *testing.T) {
342342
}
343343
}
344344

345+
func TestMagefilesFolder(t *testing.T) {
346+
resetTerm()
347+
wd, err := os.Getwd()
348+
t.Log(wd)
349+
if err != nil {
350+
t.Fatalf("finding current working directory: %v", err)
351+
}
352+
if err := os.Chdir("testdata/with_magefiles_folder"); err != nil {
353+
t.Fatalf("changing to magefolders tests data: %v", err)
354+
}
355+
// restore previous state
356+
defer os.Chdir(wd)
357+
358+
stderr := &bytes.Buffer{}
359+
stdout := &bytes.Buffer{}
360+
inv := Invocation{
361+
Dir: "",
362+
Stdout: stdout,
363+
Stderr: stderr,
364+
List: true,
365+
}
366+
code := Invoke(inv)
367+
if code != 0 {
368+
t.Errorf("expected to exit with code 0, but got %v, stderr: %s", code, stderr)
369+
}
370+
expected := "Targets:\n build \n"
371+
actual := stdout.String()
372+
if actual != expected {
373+
t.Fatalf("expected %q but got %q", expected, actual)
374+
}
375+
}
376+
377+
func TestMagefilesFolderMixedWithMagefiles(t *testing.T) {
378+
resetTerm()
379+
wd, err := os.Getwd()
380+
t.Log(wd)
381+
if err != nil {
382+
t.Fatalf("finding current working directory: %v", err)
383+
}
384+
if err := os.Chdir("testdata/with_magefiles_folder_and_mage_files_in_dot"); err != nil {
385+
t.Fatalf("changing to magefolders tests data: %v", err)
386+
}
387+
// restore previous state
388+
defer os.Chdir(wd)
389+
390+
stderr := &bytes.Buffer{}
391+
stdout := &bytes.Buffer{}
392+
inv := Invocation{
393+
Dir: "",
394+
Stdout: stdout,
395+
Stderr: stderr,
396+
List: true,
397+
}
398+
code := Invoke(inv)
399+
if code != 0 {
400+
t.Errorf("expected to exit with code 0, but got %v, stderr: %s", code, stderr)
401+
}
402+
expected := "Targets:\n build \n"
403+
actual := stdout.String()
404+
if actual != expected {
405+
t.Fatalf("expected %q but got %q", expected, actual)
406+
}
407+
408+
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"
409+
actualErr := stderr.String()
410+
if actualErr != expectedErr {
411+
t.Fatalf("expected Warning %q but got %q", expectedErr, actualErr)
412+
}
413+
}
414+
415+
func TestUntaggedMagefilesFolder(t *testing.T) {
416+
resetTerm()
417+
wd, err := os.Getwd()
418+
t.Log(wd)
419+
if err != nil {
420+
t.Fatalf("finding current working directory: %v", err)
421+
}
422+
if err := os.Chdir("testdata/with_untagged_magefiles_folder"); err != nil {
423+
t.Fatalf("changing to magefolders tests data: %v", err)
424+
}
425+
// restore previous state
426+
defer os.Chdir(wd)
427+
428+
stderr := &bytes.Buffer{}
429+
stdout := &bytes.Buffer{}
430+
inv := Invocation{
431+
Dir: "",
432+
Stdout: stdout,
433+
Stderr: stderr,
434+
List: true,
435+
}
436+
code := Invoke(inv)
437+
if code != 0 {
438+
t.Errorf("expected to exit with code 0, but got %v, stderr: %s", code, stderr)
439+
}
440+
expected := "Targets:\n build \n"
441+
actual := stdout.String()
442+
if actual != expected {
443+
t.Fatalf("expected %q but got %q", expected, actual)
444+
}
445+
}
446+
447+
func TestMixedTaggingMagefilesFolder(t *testing.T) {
448+
resetTerm()
449+
wd, err := os.Getwd()
450+
t.Log(wd)
451+
if err != nil {
452+
t.Fatalf("finding current working directory: %v", err)
453+
}
454+
if err := os.Chdir("testdata/with_mixtagged_magefiles_folder"); err != nil {
455+
t.Fatalf("changing to magefolders tests data: %v", err)
456+
}
457+
// restore previous state
458+
defer os.Chdir(wd)
459+
460+
stderr := &bytes.Buffer{}
461+
stdout := &bytes.Buffer{}
462+
inv := Invocation{
463+
Dir: "",
464+
Stdout: stdout,
465+
Stderr: stderr,
466+
List: true,
467+
}
468+
code := Invoke(inv)
469+
if code != 0 {
470+
t.Errorf("expected to exit with code 0, but got %v, stderr: %s", code, stderr)
471+
}
472+
expected := "Targets:\n build \n untaggedBuild \n"
473+
actual := stdout.String()
474+
if actual != expected {
475+
t.Fatalf("expected %q but got %q", expected, actual)
476+
}
477+
}
478+
345479
func TestGoRun(t *testing.T) {
346480
c := exec.Command("go", "run", "main.go")
347481
c.Dir = "./testdata"
@@ -1615,7 +1749,7 @@ func TestWrongDependency(t *testing.T) {
16151749
}
16161750
}
16171751

1618-
/// This code liberally borrowed from https://github.com/rsc/goversion/blob/master/version/exe.go
1752+
// / This code liberally borrowed from https://github.com/rsc/goversion/blob/master/version/exe.go
16191753

16201754
type (
16211755
exeType int

0 commit comments

Comments
 (0)