Repository: magefile/mage
Pull request #187 (2018-10-06 13:22:44)

implement importing magefiles
This is stll a work in progress (needs tests and validation code) but the core works.   What you do is underscore import the package that has the mage targets you want in the current magefile, and then add `// mage:import` as a comment on the import.  Note that the package you import will include all files, not just those with `//+build mage`. This is unfortunately a restriction of the go compiler and not something that I can really get around.

By default mage:import drops the targets into the root namespace of this magefile, but if you'd like to make a namespace for those targets you can do so by adding a namespace after the mage:import statement.  For example, `// mage:import build` would prefix the targets in that
package with `build:`

full example:

```go
// +build mage

import (
    "fmt"

    // targets imported as if they were defined in this file
    // mage:import
    _ "github.com/foo/bar/mage/common" 
   
    // targets imported as if they were in a "build" namespace in this file
    // mage:import build
    _ "github.com/foo/bar/mage/builder"  
)
```

It technically doesn't have to be an underscore import, any import will work, but the other imports will cause "unused import" errors (if they're not used).



Patch:
diff --git a/mage/main.go b/mage/main.go
index 6b8e9ed..b2aec2c 100644
--- a/mage/main.go
+++ b/mage/main.go
@@ -32,7 +32,7 @@ const magicRebuildKey = "v0.3"
 
 var output = template.Must(template.New("").Funcs(map[string]interface{}{
 	"lower": strings.ToLower,
-	"lowerfirst": func(s string) string {
+	"lowerFirst": func(s string) string {
 		parts := strings.Split(s, ":")
 		for i, t := range parts {
 			r := []rune(t)
@@ -131,11 +131,6 @@ func ParseAndRun(stdout, stderr io.Writer, stdin io.Reader, args []string) int {
 			out.Println("Error:", err)
 			return 1
 		}
-		if err := removeContents(filepath.Join(inv.CacheDir, mainfileSubdir)); err != nil {
-			out.Println("Error:", err)
-			return 1
-		}
-
 		out.Println(inv.CacheDir, "cleaned")
 		return 0
 	case CompileStatic:
@@ -334,8 +329,42 @@ func Invoke(inv Invocation) int {
 		return 1
 	}
 
+	imports, err := getNamedImports(inv.GoCmd, info.Imports)
+	if err != nil {
+		errlog.Println(err)
+		return 1
+	}
+	for _, s := range info.RootImports {
+		imp, err := getImport(inv.GoCmd, s, "")
+		if err != nil {
+			errlog.Println(err)
+			return 1
+		}
+		imports = append(imports, imp)
+	}
+
+	if !checkDupes(info, imports) {
+		return 1
+	}
+
+	// have to set unique package names on imports
+	used := map[string]bool{}
+	for _, imp := range imports {
+		unique := imp.Name + "_mageimport"
+		x := 1
+		for used[unique] {
+			unique = fmt.Sprintf("%s_mageimport%d", imp.Name, x)
+			x++
+		}
+		used[unique] = true
+		imp.UniqueName = unique
+		for _, f := range imp.Info.Funcs {
+			f.Package = unique
+		}
+	}
+
 	main := filepath.Join(inv.Dir, mainfile)
-	err = GenerateMainfile(main, inv.CacheDir, info)
+	err = GenerateMainfile(main, inv.CacheDir, info, imports)
 	if err != nil {
 		errlog.Println("Error:", err)
 		return 1
@@ -364,20 +393,96 @@ func Invoke(inv Invocation) int {
 	return RunCompiled(inv, exePath, errlog)
 }
 
-func moveMainToCache(cachedir, main, hash string) {
-	debug.Println("moving main file to cache:", cachedMainfile(cachedir, hash))
-	if err := os.Rename(main, cachedMainfile(cachedir, hash)); err != nil && !os.IsNotExist(err) {
-		debug.Println("error caching main file: ", err)
+func checkDupes(info *parse.PkgInfo, imports []*Import) (ok bool) {
+	funcs := map[string][]*parse.Function{}
+	for _, f := range info.Funcs {
+		funcs[strings.ToLower(f.TargetName())] = append(funcs[strings.ToLower(f.TargetName())], f)
+	}
+	for _, imp := range imports {
+		for _, f := range imp.Info.Funcs {
+			target := strings.ToLower(f.TargetName())
+			funcs[target] = append(funcs[target], f)
+		}
+	}
+	for alias, f := range info.Aliases {
+		if len(funcs[alias]) != 0 {
+			var ids []string
+			for _, f := range funcs[alias] {
+				ids = append(ids, f.ID())
+			}
+			fmt.Printf("ERROR: alias %q duplicates existing target(s): %s\n", alias, strings.Join(ids, ", "))
+		}
+		funcs[alias] = append(funcs[alias], f)
+	}
+	var dupes []string
+	for target, list := range funcs {
+		if len(list) > 1 {
+			dupes = append(dupes, target)
+		}
 	}
+	if len(dupes) == 0 {
+		return true
+	}
+	for _, d := range dupes {
+		var ids []string
+		for _, f := range funcs[d] {
+			ids = append(ids, f.ID())
+		}
+		fmt.Printf("ERROR: %q target has multiple definitions: %s\n", d, strings.Join(ids, ", "))
+	}
+	return false
+}
+
+func getNamedImports(gocmd string, pkgs map[string]string) ([]*Import, error) {
+	var imports []*Import
+	for alias, pkg := range pkgs {
+		debug.Printf("getting import package %q, alias %q", pkg, alias)
+		imp, err := getImport(gocmd, pkg, alias)
+		if err != nil {
+			return nil, err
+		}
+		imports = append(imports, imp)
+	}
+	return imports, nil
+}
+
+func getImport(gocmd, importpath, alias string) (*Import, error) {
+	out, err := outputDebug(gocmd, "list", "-tags=mage", "-f", "{{.Dir}}||{{.Name}}", importpath)
+	if err != nil {
+		return nil, err
+	}
+	parts := strings.Split(out, "||")
+	if len(parts) != 2 {
+		return nil, fmt.Errorf("incorrect data from go list: %s", out)
+	}
+	dir, name := parts[0], parts[1]
+	debug.Printf("parsing imported package %q from dir %q", importpath, dir)
+	info, err := parse.Package(dir, nil)
+	if err != nil {
+		return nil, err
+	}
+	for i := range info.Funcs {
+		debug.Printf("setting alias %q and package %q on func %v", alias, name, info.Funcs[i].Name)
+		info.Funcs[i].PkgAlias = alias
+		info.Funcs[i].ImportPath = importpath
+	}
+	return &Import{Alias: alias, Name: name, Path: importpath, Info: *info}, nil
+}
+
+type Import struct {
+	Alias      string
+	Name       string
+	UniqueName string // a name unique across all imports
+	Path       string
+	Info       parse.PkgInfo
 }
 
 type data struct {
-	Description  string
-	Funcs        []parse.Function
-	DefaultError bool
-	Default      string
-	DefaultFunc  parse.Function
-	Aliases      map[string]parse.Function
+	Description string
+	Funcs       []*parse.Function
+	DefaultFunc parse.Function
+	Aliases     map[string]*parse.Function
+	Imports     []*Import
 }
 
 func goVersion(gocmd string) (string, error) {
@@ -463,8 +568,8 @@ func Compile(magePath, goCmd, compileTo string, gofiles []string, isDebug bool,
 	for i := range gofiles {
 		gofiles[i] = filepath.Base(gofiles[i])
 	}
-	debug.Printf("running %s build -o %s %s", goCmd, compileTo, strings.Join(gofiles, " "))
-	c := exec.Command(goCmd, append([]string{"build", "-o", compileTo}, gofiles...)...)
+	debug.Printf("running %s -tag=mage build -o %s %s", goCmd, compileTo, strings.Join(gofiles, " "))
+	c := exec.Command(goCmd, append([]string{"build", "-tags=mage", "-o", compileTo}, gofiles...)...)
 	c.Env = os.Environ()
 	c.Stderr = stderr
 	c.Stdout = stdout
@@ -509,16 +614,10 @@ func outputDebug(cmd string, args ...string) (string, error) {
 	return strings.TrimSpace(buf.String()), nil
 }
 
-const mainfileSubdir = "mainfiles"
-
-func cachedMainfile(cachedir, hash string) string {
-	return filepath.Join(cachedir, mainfileSubdir, hash)
-}
-
 // GenerateMainfile generates the mage mainfile at path.  Because go build's
 // cache cares about modtimes, we squirrel away our mainfiles in the cachedir
 // and move them back as needed.
-func GenerateMainfile(path, cachedir string, info *parse.PkgInfo) error {
+func GenerateMainfile(path, cachedir string, info *parse.PkgInfo, imports []*Import) error {
 	debug.Println("Creating mainfile at", path)
 
 	f, err := os.Create(path)
@@ -529,8 +628,12 @@ func GenerateMainfile(path, cachedir string, info *parse.PkgInfo) error {
 	data := data{
 		Description: info.Description,
 		Funcs:       info.Funcs,
-		DefaultFunc: info.DefaultFunc,
 		Aliases:     info.Aliases,
+		Imports:     imports,
+	}
+
+	if info.DefaultFunc != nil {
+		data.DefaultFunc = *info.DefaultFunc
 	}
 
 	debug.Println("writing new file at", path)
diff --git a/mage/template.go b/mage/template.go
index f14a3b8..450554d 100644
--- a/mage/template.go
+++ b/mage/template.go
@@ -15,24 +15,32 @@ import (
 	"strings"
 	"text/tabwriter"
 	"time"
+	{{range .Imports}}{{.UniqueName}} "{{.Path}}"
+	{{end}}
 )
 
 func main() {
 	// These functions are local variables to avoid name conflicts with 
 	// magefiles.
 	list := func() error {
-		{{with .Description}}fmt.Println(` + "`{{.}}\n`" + `){{end}}
+		{{with .Description}}fmt.Println(` + "`{{.}}\n`" + `)
+		{{- end}}
 		{{- $default := .DefaultFunc}}
 		w := tabwriter.NewWriter(os.Stdout, 0, 4, 4, ' ', 0)
 		fmt.Println("Targets:")
 		{{- range .Funcs}}
-		fmt.Fprintln(w, "  {{lowerfirst .TemplateName}}{{if and (eq .Name $default.Name) (eq .Receiver $default.Receiver)}}*{{end}}\t" + {{printf "%q" .Synopsis}})
+			fmt.Fprintln(w, "  {{lowerFirst .TargetName}}{{if and (eq .Name $default.Name) (eq .Receiver $default.Receiver)}}*{{end}}\t" + {{printf "%q" .Synopsis}})
+		{{- end}}
+		{{- range .Imports}}{{$imp := .}}
+			{{- range .Info.Funcs}}
+			fmt.Fprintln(w, "  {{lowerFirst .TargetName}}{{if and (eq .Name $default.Name) (eq .Receiver $default.Receiver)}}*{{end}}\t" + {{printf "%q" .Synopsis}})
+			{{end}}
 		{{- end}}
 		err := w.Flush()
 		{{- if .DefaultFunc.Name}}
-		if err == nil {
-			fmt.Println("\n* default target")
-		}
+			if err == nil {
+				fmt.Println("\n* default target")
+			}
 		{{- end}}
 		return err
 	}
@@ -118,7 +126,14 @@ func main() {
 	targets := map[string]bool {
 		{{range $alias, $funci := .Aliases}}"{{lower $alias}}": true,
 		{{end}}
-		{{range .Funcs}}"{{lower .TemplateName}}": true,
+		{{range .Funcs}}"{{lower .TargetName}}": true,
+		{{end}}
+		{{range .Imports}}
+			{{$imp := .}}
+			{{range $alias, $funci := .Info.Aliases}}"{{if ne $imp.Alias "."}}{{lower $imp.Alias}}:{{end}}{{lower $alias}}": true,
+			{{end}}
+			{{range .Info.Funcs}}"{{lower .TargetName}}": true,
+			{{end}}
 		{{end}}
 	}
 
@@ -143,8 +158,8 @@ func main() {
 			os.Exit(1)
 		}
 		switch strings.ToLower(os.Args[1]) {
-			{{range .Funcs}}case "{{lower .TemplateName}}":
-				fmt.Print("mage {{lower .TemplateName}}:\n\n")
+			{{range .Funcs}}case "{{lower .TargetName}}":
+				fmt.Print("mage {{lower .TargetName}}:\n\n")
 				{{if ne .Comment "" -}}
 				fmt.Println({{printf "%q" .Comment}})
 				fmt.Println()
@@ -175,7 +190,7 @@ func main() {
 			}
 			return	
 		}
-		{{.DefaultFunc.TemplateString}}
+		{{.DefaultFunc.ExecCode}}
 		handleError(logger, err)
 		return
 	{{- else}}
@@ -189,18 +204,29 @@ func main() {
 	for _, target := range os.Args[1:] {
 		switch strings.ToLower(target) {
 		{{range $alias, $func := .Aliases}}
-		case "{{lower $alias}}":
-			target = "{{with $func.Receiver}}{{.}}:{{end}}{{$func.Name}}"
+			case "{{lower $alias}}":
+				target = "{{$func.TargetName}}"
 		{{- end}}
 		}
 		switch strings.ToLower(target) {
 		{{range .Funcs }}
-		case "{{lower .TemplateName}}":
-			if verbose {
-				logger.Println("Running target:", "{{.TemplateName}}")
-			}
-			{{.TemplateString}}
-			handleError(logger, err)
+			case "{{lower .TargetName}}":
+				if verbose {
+					logger.Println("Running target:", "{{.TargetName}}")
+				}
+				{{.ExecCode}}
+				handleError(logger, err)
+		{{- end}}
+		{{range .Imports}}
+		{{$imp := .}}
+			{{range .Info.Funcs }}
+				case "{{lower .TargetName}}":
+					if verbose {
+						logger.Println("Running target:", "{{.TargetName}}")
+					}
+					{{.ExecCode}}
+					handleError(logger, err)
+			{{- end}}
 		{{- end}}
 		default:
 			// should be impossible since we check this above.
diff --git a/magefile.go b/magefile.go
index 424acca..12e9a48 100644
--- a/magefile.go
+++ b/magefile.go
@@ -19,10 +19,6 @@ import (
 	"github.com/magefile/mage/sh"
 )
 
-var Aliases = map[string]interface{}{
-	"foo": Foo.Build,
-}
-
 // Runs "go install" for mage.  This generates the version info the binary.
 func Install() error {
 	name := "mage"
@@ -102,9 +98,3 @@ func hash() string {
 	hash, _ := sh.Output("git", "rev-parse", "--short", "HEAD")
 	return hash
 }
-
-type Foo mg.Namespace
-
-func (Foo) Build() {
-
-}
diff --git a/parse/parse.go b/parse/parse.go
index 2ac52fe..2ae3fd4 100644
--- a/parse/parse.go
+++ b/parse/parse.go
@@ -1,6 +1,7 @@
 package parse
 
 import (
+	"bytes"
 	"errors"
 	"fmt"
 	"go/ast"
@@ -10,10 +11,13 @@ import (
 	"io/ioutil"
 	"log"
 	"os"
+	"os/exec"
 	"strings"
 	"time"
 )
 
+const importTag = "mage:import"
+
 var debug = log.New(ioutil.Discard, "DEBUG: ", log.Ltime|log.Lmicroseconds)
 
 // EnableDebug turns on debug logging.
@@ -25,73 +29,102 @@ func EnableDebug() {
 // parsing rules.
 type PkgInfo struct {
 	Description string
-	Funcs       []Function
-	DefaultFunc Function
-	Aliases     map[string]Function
+	Funcs       []*Function
+	DefaultFunc *Function
+	Aliases     map[string]*Function
+	Imports     map[string]string
+	RootImports []string
 }
 
 // Function represented a job function from a mage file
 type Function struct {
-	Name      string
-	Receiver  string
-	IsError   bool
-	IsContext bool
-	Synopsis  string
-	Comment   string
+	PkgAlias   string
+	Package    string
+	ImportPath string
+	Name       string
+	Receiver   string
+	IsError    bool
+	IsContext  bool
+	Synopsis   string
+	Comment    string
 }
 
-// TemplateName returns the invocation name, supporting namespaced functions.
-func (f Function) TemplateName() string {
+// ID returns user-readable information about where this function is defined.
+func (f Function) ID() string {
+	path := "<current>"
+	if f.ImportPath != "" {
+		path = f.ImportPath
+	}
+	receiver := ""
 	if f.Receiver != "" {
-		return strings.ToLower(fmt.Sprintf("%s:%s", f.Receiver, f.Name))
+		receiver = f.Receiver + "."
 	}
+	return fmt.Sprintf("%s.%s%s", path, receiver, f.Name)
+}
+
+// TargetName returns the name of the target as it should appear when used from
+// the mage cli.  It is always lowercase.
+func (f Function) TargetName() string {
+	var names []string
 
-	return f.Name
+	for _, s := range []string{f.PkgAlias, f.Receiver, f.Name} {
+		if s != "" {
+			names = append(names, s)
+		}
+	}
+	return strings.Join(names, ":")
 }
 
-// TemplateString returns code for the template switch to run the target.
+// ExecCode returns code for the template switch to run the target.
 // It wraps each target call to match the func(context.Context) error that
 // runTarget requires.
-func (f Function) TemplateString() string {
+func (f Function) ExecCode() (string, error) {
 	name := f.Name
 	if f.Receiver != "" {
-		name = fmt.Sprintf("%s{}.%s", f.Receiver, f.Name)
+		name = f.Receiver + "{}." + name
+	}
+	if f.Package != "" {
+		name = f.Package + "." + name
 	}
+
 	if f.IsContext && f.IsError {
-		out := `wrapFn := func(ctx context.Context) error {
+		out := `
+			wrapFn := func(ctx context.Context) error {
 				return %s(ctx)
 			}
-			err := runTarget(wrapFn)`
-		return fmt.Sprintf(out, name)
+			err := runTarget(wrapFn)`[1:]
+		return fmt.Sprintf(out, name), nil
 	}
 	if f.IsContext && !f.IsError {
-		out := `wrapFn := func(ctx context.Context) error {
+		out := `
+			wrapFn := func(ctx context.Context) error {
 				%s(ctx)
 				return nil
 			}
-			err := runTarget(wrapFn)`
-		return fmt.Sprintf(out, name)
+			err := runTarget(wrapFn)`[1:]
+		return fmt.Sprintf(out, name), nil
 	}
 	if !f.IsContext && f.IsError {
-		out := `wrapFn := func(ctx context.Context) error {
+		out := `
+			wrapFn := func(ctx context.Context) error {
 				return %s()
 			}
-			err := runTarget(wrapFn)`
-		return fmt.Sprintf(out, name)
+			err := runTarget(wrapFn)`[1:]
+		return fmt.Sprintf(out, name), nil
 	}
 	if !f.IsContext && !f.IsError {
-		out := `wrapFn := func(ctx context.Context) error {
+		out := `
+			wrapFn := func(ctx context.Context) error {
 				%s()
 				return nil
 			}
-			err := runTarget(wrapFn)`
-		return fmt.Sprintf(out, name)
+			err := runTarget(wrapFn)`[1:]
+		return fmt.Sprintf(out, name), nil
 	}
-	return `fmt.Printf("Error formatting job code\n")
-	os.Exit(1)`
+	return "", fmt.Errorf("Error formatting ExecCode code for %#v", f)
 }
 
-// Package parses a package
+// Package parses a package.  If files is non-empty, it will only parse the files given.
 func Package(path string, files []string) (*PkgInfo, error) {
 	start := time.Now()
 	defer func() {
@@ -102,42 +135,35 @@ func Package(path string, files []string) (*PkgInfo, error) {
 	if err != nil {
 		return nil, err
 	}
-
 	p := doc.New(pkg, "./", 0)
 	pi := &PkgInfo{
 		Description: toOneLine(p.Doc),
 	}
 
-typeloop:
-	for _, t := range p.Types {
-		for _, s := range t.Decl.Specs {
-			if id, ok := s.(*ast.TypeSpec); ok {
-				if sel, ok := id.Type.(*ast.SelectorExpr); ok {
-					if ident, ok := sel.X.(*ast.Ident); ok {
-						if ident.Name != "mg" || sel.Sel.Name != "Namespace" {
-							continue typeloop
-						}
-					}
-				}
-				break
-			}
-		}
-		for _, f := range t.Methods {
-			if !ast.IsExported(f.Name) {
-				continue
-			}
-			if typ := funcType(f.Decl.Type); typ != invalidType {
-				pi.Funcs = append(pi.Funcs, Function{
-					Name:      f.Name,
-					Receiver:  f.Recv,
-					Comment:   toOneLine(f.Doc),
-					Synopsis:  sanitizeSynopsis(f),
-					IsError:   typ == errorType || typ == contextErrorType,
-					IsContext: typ == contextVoidType || typ == contextErrorType,
-				})
+	if err := setImports(pkg, pi); err != nil {
+		return nil, err
+	}
+	setNamespaces(p, pi)
+	setFuncs(p, pi)
+
+	hasDupes, names := checkDupes(pi)
+	if hasDupes {
+		msg := "Build targets must be case insensitive, thus the following targets conflict:\n"
+		for _, v := range names {
+			if len(v) > 1 {
+				msg += "  " + strings.Join(v, ", ") + "\n"
 			}
 		}
+		return nil, errors.New(msg)
 	}
+
+	setDefault(p, pi)
+	setAliases(p, pi)
+
+	return pi, nil
+}
+
+func setFuncs(p *doc.Package, pi *PkgInfo) {
 	for _, f := range p.Funcs {
 		if f.Recv != "" {
 			debug.Printf("skipping method %s.%s", f.Recv, f.Name)
@@ -151,7 +177,7 @@ typeloop:
 		}
 		if typ := funcType(f.Decl.Type); typ != invalidType {
 			debug.Printf("found target %v", f.Name)
-			pi.Funcs = append(pi.Funcs, Function{
+			pi.Funcs = append(pi.Funcs, &Function{
 				Name:      f.Name,
 				Comment:   toOneLine(f.Doc),
 				Synopsis:  sanitizeSynopsis(f),
@@ -162,22 +188,109 @@ typeloop:
 			debug.Printf("skipping function with invalid signature func %s(%v)(%v)", f.Name, fieldNames(f.Decl.Type.Params), fieldNames(f.Decl.Type.Results))
 		}
 	}
+}
 
-	hasDupes, names := checkDupes(pi)
-	if hasDupes {
-		msg := "Build targets must be case insensitive, thus the following targets conflict:\n"
-		for _, v := range names {
-			if len(v) > 1 {
-				msg += "  " + strings.Join(v, ", ") + "\n"
+func setNamespaces(p *doc.Package, pi *PkgInfo) {
+	for _, t := range p.Types {
+		if !isNamespace(t) {
+			continue
+		}
+		debug.Printf("found namespace %s %s", p.ImportPath, t.Name)
+		for _, f := range t.Methods {
+			if !ast.IsExported(f.Name) {
+				continue
+			}
+			typ := funcType(f.Decl.Type)
+			if typ == invalidType {
+				continue
 			}
+			debug.Printf("found namespace method %s %s.%s", p.ImportPath, t.Name, f.Name)
+			pi.Funcs = append(pi.Funcs, &Function{
+				Name:      f.Name,
+				Receiver:  t.Name,
+				Comment:   toOneLine(f.Doc),
+				Synopsis:  sanitizeSynopsis(f),
+				IsError:   typ == errorType || typ == contextErrorType,
+				IsContext: typ == contextVoidType || typ == contextErrorType,
+			})
 		}
-		return nil, errors.New(msg)
 	}
+}
 
-	setDefault(p, pi)
-	setAliases(p, pi)
+func setImports(pkg *ast.Package, pi *PkgInfo) error {
+	pi.Imports = map[string]string{}
+	for _, f := range pkg.Files {
+		for _, imp := range f.Imports {
+			name, alias, ok := getImport(imp)
+			if !ok {
+				continue
+			}
+			if alias != "" {
+				debug.Printf("found %s: %s (%s)", importTag, name, alias)
+				if pi.Imports[alias] != "" {
+					return fmt.Errorf("duplicate import alias: %q", alias)
+				}
+				pi.Imports[alias] = name
+			} else {
+				debug.Printf("found %s: %s", importTag, name)
+				pi.RootImports = append(pi.RootImports, name)
+			}
+		}
+	}
+	return nil
+}
 
-	return pi, nil
+func getImport(imp *ast.ImportSpec) (path, alias string, ok bool) {
+	if imp.Doc == nil || len(imp.Doc.List) == 9 {
+		return "", "", false
+	}
+	// import is always the last comment
+	s := imp.Doc.List[len(imp.Doc.List)-1].Text
+
+	// trim comment start and normalize for anyone who has spaces or not between
+	// "//"" and the text
+	vals := strings.Fields(strings.ToLower(s[2:]))
+	if len(vals) == 0 {
+		return "", "", false
+	}
+	if vals[0] != importTag {
+		return "", "", false
+	}
+	path, ok = lit2string(imp.Path)
+	if !ok {
+		return "", "", false
+	}
+
+	switch len(vals) {
+	case 1:
+		// just the import tag, this is a root import
+		return path, "", true
+	case 2:
+		// also has an alias
+		return path, vals[1], true
+	default:
+		log.Println("warning: ignoring malformed", importTag, "for import", path)
+		return "", "", false
+	}
+}
+
+func isNamespace(t *doc.Type) bool {
+	if len(t.Decl.Specs) != 1 {
+		return false
+	}
+	id, ok := t.Decl.Specs[0].(*ast.TypeSpec)
+	if !ok {
+		return false
+	}
+	sel, ok := id.Type.(*ast.SelectorExpr)
+	if !ok {
+		return false
+	}
+	ident, ok := sel.X.(*ast.Ident)
+	if !ok {
+		return false
+	}
+	return ident.Name == "mg" && sel.Sel.Name == "Namespace"
 }
 
 func fieldNames(flist *ast.FieldList) string {
@@ -273,6 +386,28 @@ func setDefault(p *doc.Package, pi *PkgInfo) {
 	}
 }
 
+func lit2string(l *ast.BasicLit) (string, bool) {
+	if !strings.HasPrefix(l.Value, `"`) || !strings.HasSuffix(l.Value, `"`) {
+		return "", false
+	}
+	return strings.Trim(l.Value, `"`), true
+}
+
+func outputDebug(cmd string, args ...string) (string, error) {
+	buf := &bytes.Buffer{}
+	errbuf := &bytes.Buffer{}
+	debug.Println("running", cmd, strings.Join(args, " "))
+	c := exec.Command(cmd, args...)
+	c.Env = os.Environ()
+	c.Stderr = errbuf
+	c.Stdout = buf
+	if err := c.Run(); err != nil {
+		debug.Print("error running '", cmd, strings.Join(args, " "), "': ", err, ": ", errbuf)
+		return "", err
+	}
+	return strings.TrimSpace(buf.String()), nil
+}
+
 func setAliases(p *doc.Package, pi *PkgInfo) {
 	for _, v := range p.Vars {
 		for x, name := range v.Names {
@@ -292,7 +427,7 @@ func setAliases(p *doc.Package, pi *PkgInfo) {
 				log.Println("warning: aliases declaration is not a map")
 				return
 			}
-			pi.Aliases = map[string]Function{}
+			pi.Aliases = map[string]*Function{}
 			for _, elem := range comp.Elts {
 				kv, ok := elem.(*ast.KeyValueExpr)
 				if !ok {
@@ -315,8 +450,13 @@ func setAliases(p *doc.Package, pi *PkgInfo) {
 					receiver = fmt.Sprintf("%s", v.X)
 				default:
 					log.Printf("warning: target for alias %s is not a function", k.Value)
+					continue
+				}
+				alias, ok := lit2string(k)
+				if !ok {
+					log.Println("warning: malformed alias declaration for alias", k.Value)
+					continue
 				}
-				alias := strings.Trim(k.Value, "\"")
 				valid := false
 				for _, f := range pi.Funcs {
 					if f.Name == name && f.Receiver == receiver {
@@ -336,13 +476,16 @@ func setAliases(p *doc.Package, pi *PkgInfo) {
 
 // getPackage returns the non-test package at the given path.
 func getPackage(path string, files []string, fset *token.FileSet) (*ast.Package, error) {
-	fm := make(map[string]bool, len(files))
-	for _, f := range files {
-		fm[f] = true
-	}
+	var filter func(f os.FileInfo) bool
+	if len(files) > 0 {
+		fm := make(map[string]bool, len(files))
+		for _, f := range files {
+			fm[f] = true
+		}
 
-	filter := func(f os.FileInfo) bool {
-		return fm[f.Name()]
+		filter = func(f os.FileInfo) bool {
+			return fm[f.Name()]
+		}
 	}
 
 	pkgs, err := parser.ParseDir(fset, path, filter, parser.ParseComments)
diff --git a/site/content/importing/_index.en.md b/site/content/importing/_index.en.md
new file mode 100644
index 0000000..91c2ede
--- /dev/null
+++ b/site/content/importing/_index.en.md
@@ -0,0 +1,59 @@
++++
+title = "Importing Targets"
+weight = 11
++++
+
+Mage allows you to import targets from another package into the current
+magefile.  This is very useful when you have common targets that you'd like to
+be able to reuse across many repos.
+
+## Imported Package Restrictions
+
+To import targets from another package, that package must *not* be a main
+package.  i.e. it cannot be `package main`.  This is in contrast to a normal
+mage package that must be `package main`.  The reason is that the go tool won't
+let you import a main package.
+
+In addition, all files will be imported, not just those tagged with the
+`//+build mage` build tag.  Again, this is a restriction of the go tool.  Mage
+can build only the `.go` files in the current directory that have the `mage`
+build tag, but when importing code from other directories, it's simply not
+possible.  This means that any exported function (in any file) that matches
+Mage's allowed formats will be picked up as a target. 
+
+Aliases and defaults in the imported package will be ignored. 
+
+Other than these differences, you can write targets in those packages just
+like a normal magefile.
+
+## Two Ways to Import
+
+Importing targets from a package simply requires adding a `// mage:import`
+comment on an import statement in your magefile.  If there is a name after this
+tag, the targets will be imported into what is effectively like a namespace.
+
+```go
+import (
+    // mage:import
+    _ "example.com/me/foobar" 
+    // mage:import build
+    "example.com/me/builder"
+)
+```
+
+The first mage import above will add the targets from the foobar package to the
+root namespace of your current magefile.  Thus, if there's a `func Deploy()` in
+foobar, your magefile will now have a `deploy` target.
+
+The second import above will create a `build` namespace for the targets in the
+`builder` package.  Thus, if there's a `func All()` in builder, your magefile
+will now have a `build:all` target.
+
+Note that you can use imported package targets as dependencies with `mg.Deps`,
+just like any other function works in `mg.Deps`, so you could have
+`mg.Deps(builder.All)`.
+
+If you don't need to actually use the package in your root magefile, simply make
+the import an underscore import like the first import above.
+
+


Test patch:
diff --git a/mage/import_test.go b/mage/import_test.go
new file mode 100644
index 0000000..1d9704b
--- /dev/null
+++ b/mage/import_test.go
@@ -0,0 +1,142 @@
+package mage
+
+import (
+	"bytes"
+	"testing"
+)
+
+func TestMageImportsList(t *testing.T) {
+	stdout := &bytes.Buffer{}
+	stderr := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/mageimport",
+		Stdout: stdout,
+		Stderr: stderr,
+		List:   true,
+	}
+
+	code := Invoke(inv)
+	if code != 0 {
+		t.Fatalf("expected to exit with code 0, but got %v, stderr:\n%s", code, stderr)
+	}
+	actual := stdout.String()
+	expected := `
+Targets:
+  root               
+  zz:nS:deploy2      deploys stuff.
+  zz:buildSubdir2    Builds stuff.
+  nS:deploy          deploys stuff.
+  buildSubdir        Builds stuff.
+`[1:]
+
+	if actual != expected {
+		t.Logf("expected: %q", expected)
+		t.Logf("  actual: %q", actual)
+		t.Fatalf("expected:\n%v\n\ngot:\n%v", expected, actual)
+	}
+}
+
+func TestMageImportsRoot(t *testing.T) {
+	stdout := &bytes.Buffer{}
+	stderr := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/mageimport",
+		Stdout: stdout,
+		Stderr: stderr,
+		Args:   []string{"root"},
+	}
+
+	code := Invoke(inv)
+	if code != 0 {
+		t.Fatalf("expected to exit with code 0, but got %v, stderr:\n%s", code, stderr)
+	}
+	actual := stdout.String()
+	expected := "root\n"
+	if actual != expected {
+		t.Fatalf("expected: %q got: %q", expected, actual)
+	}
+}
+
+func TestMageImportsNamedNS(t *testing.T) {
+	stdout := &bytes.Buffer{}
+	stderr := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/mageimport",
+		Stdout: stdout,
+		Stderr: stderr,
+		Args:   []string{"zz:nS:deploy2"},
+	}
+
+	code := Invoke(inv)
+	if code != 0 {
+		t.Fatalf("expected to exit with code 0, but got %v, stderr:\n%s", code, stderr)
+	}
+	actual := stdout.String()
+	expected := "deploy2\n"
+	if actual != expected {
+		t.Fatalf("expected: %q got: %q", expected, actual)
+	}
+}
+
+func TestMageImportsNamedRoot(t *testing.T) {
+	stdout := &bytes.Buffer{}
+	stderr := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/mageimport",
+		Stdout: stdout,
+		Stderr: stderr,
+		Args:   []string{"zz:buildSubdir2"},
+	}
+
+	code := Invoke(inv)
+	if code != 0 {
+		t.Fatalf("expected to exit with code 0, but got %v, stderr:\n%s", code, stderr)
+	}
+	actual := stdout.String()
+	expected := "buildsubdir2\n"
+	if actual != expected {
+		t.Fatalf("expected: %q got: %q", expected, actual)
+	}
+}
+
+func TestMageImportsRootImportNS(t *testing.T) {
+	stdout := &bytes.Buffer{}
+	stderr := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/mageimport",
+		Stdout: stdout,
+		Stderr: stderr,
+		Args:   []string{"nS:deploy"},
+	}
+
+	code := Invoke(inv)
+	if code != 0 {
+		t.Fatalf("expected to exit with code 0, but got %v, stderr:\n%s", code, stderr)
+	}
+	actual := stdout.String()
+	expected := "deploy\n"
+	if actual != expected {
+		t.Fatalf("expected: %q got: %q", expected, actual)
+	}
+}
+
+func TestMageImportsRootImport(t *testing.T) {
+	stdout := &bytes.Buffer{}
+	stderr := &bytes.Buffer{}
+	inv := Invocation{
+		Dir:    "./testdata/mageimport",
+		Stdout: stdout,
+		Stderr: stderr,
+		Args:   []string{"buildSubdir"},
+	}
+
+	code := Invoke(inv)
+	if code != 0 {
+		t.Fatalf("expected to exit with code 0, but got %v, stderr:\n%s", code, stderr)
+	}
+	actual := stdout.String()
+	expected := "buildsubdir\n"
+	if actual != expected {
+		t.Fatalf("expected: %q got: %q", expected, actual)
+	}
+}
diff --git a/mage/testdata/mageimport/magefile.go b/mage/testdata/mageimport/magefile.go
new file mode 100644
index 0000000..cf6835d
--- /dev/null
+++ b/mage/testdata/mageimport/magefile.go
@@ -0,0 +1,26 @@
+//+build mage
+
+package main
+
+// important things to note:
+// * these two packages have the same package name, so they'll conflict
+// when imported.
+// * one is imported with underscore and one is imported normally.
+//
+// they should still work normally as mageimports
+
+import (
+	"fmt"
+
+	// mage:import
+	_ "github.com/magefile/mage/mage/testdata/mageimport/subdir1"
+	// mage:import zz
+	"github.com/magefile/mage/mage/testdata/mageimport/subdir2"
+)
+
+// just something to keep the import from being unused.
+var _ = mage.BuildSubdir2
+
+func Root() {
+	fmt.Println("root")
+}
diff --git a/mage/testdata/mageimport/subdir1/mage.go b/mage/testdata/mageimport/subdir1/mage.go
new file mode 100644
index 0000000..0dae1b5
--- /dev/null
+++ b/mage/testdata/mageimport/subdir1/mage.go
@@ -0,0 +1,20 @@
+package mage
+
+import (
+	"fmt"
+
+	"github.com/magefile/mage/mg"
+)
+
+// BuildSubdir Builds stuff.
+func BuildSubdir() {
+	fmt.Println("buildsubdir")
+}
+
+// NS is a namespace.
+type NS mg.Namespace
+
+// Deploy deploys stuff.
+func (NS) Deploy() {
+	fmt.Println("deploy")
+}
diff --git a/mage/testdata/mageimport/subdir2/mage.go b/mage/testdata/mageimport/subdir2/mage.go
new file mode 100644
index 0000000..39f699c
--- /dev/null
+++ b/mage/testdata/mageimport/subdir2/mage.go
@@ -0,0 +1,20 @@
+package mage
+
+import (
+	"fmt"
+
+	"github.com/magefile/mage/mg"
+)
+
+// BuildSubdir2 Builds stuff.
+func BuildSubdir2() {
+	fmt.Println("buildsubdir2")
+}
+
+// NS is a namespace.
+type NS mg.Namespace
+
+// Deploy2 deploys stuff.
+func (NS) Deploy2() {
+	fmt.Println("deploy2")
+}
diff --git a/parse/parse_test.go b/parse/parse_test.go
index 56b9d33..f8b3d2a 100644
--- a/parse/parse_test.go
+++ b/parse/parse_test.go
@@ -78,9 +78,11 @@ func TestParse(t *testing.T) {
 	for _, fn := range expected {
 		found := false
 		for _, infoFn := range info.Funcs {
-			if reflect.DeepEqual(fn, infoFn) {
+			if reflect.DeepEqual(fn, *infoFn) {
 				found = true
 				break
+			} else {
+				t.Logf("%#v", infoFn)
 			}
 		}
 		if !found {