Skip to content

Commit c7b4ff9

Browse files
authored
add code so mg.Deps understands namespaces (#163)
* add code so mg.Deps understands namespaces * unexport all the functype stuff
1 parent 832a425 commit c7b4ff9

File tree

10 files changed

+355
-169
lines changed

10 files changed

+355
-169
lines changed

go.sum

Whitespace-only changes.

mage/main_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,3 +778,39 @@ func minorVer(t *testing.T, v string) int {
778778
}
779779
return a
780780
}
781+
782+
func TestNamespaceDep(t *testing.T) {
783+
stdout := &bytes.Buffer{}
784+
inv := Invocation{
785+
Dir: "./testdata/namespaces",
786+
Stderr: ioutil.Discard,
787+
Stdout: stdout,
788+
Args: []string{"TestNamespaceDep"},
789+
}
790+
code := Invoke(inv)
791+
if code != 0 {
792+
t.Fatalf("expected 0, but got %v", code)
793+
}
794+
expected := "hi!\n"
795+
if stdout.String() != expected {
796+
t.Fatalf("expected %q, but got %q", expected, stdout.String())
797+
}
798+
}
799+
800+
func TestNamespace(t *testing.T) {
801+
stdout := &bytes.Buffer{}
802+
inv := Invocation{
803+
Dir: "./testdata/namespaces",
804+
Stderr: ioutil.Discard,
805+
Stdout: stdout,
806+
Args: []string{"namespace:SayHi"},
807+
}
808+
code := Invoke(inv)
809+
if code != 0 {
810+
t.Fatalf("expected 0, but got %v", code)
811+
}
812+
expected := "hi!\n"
813+
if stdout.String() != expected {
814+
t.Fatalf("expected %q, but got %q", expected, stdout.String())
815+
}
816+
}

mage/testdata/namespaces/magefile.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//+build mage
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
8+
"github.com/magefile/mage/mg"
9+
)
10+
11+
func TestNamespaceDep() {
12+
mg.Deps(Namespace.SayHi)
13+
}
14+
15+
type Namespace mg.Namespace
16+
17+
func (Namespace) SayHi() {
18+
fmt.Println("hi!")
19+
}

mg/deps.go

Lines changed: 167 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,22 @@ import (
99
"runtime"
1010
"strings"
1111
"sync"
12+
)
13+
14+
// funcType indicates a prototype of build job function
15+
type funcType int
1216

13-
"github.com/magefile/mage/types"
17+
// funcTypes
18+
const (
19+
invalidType funcType = iota
20+
voidType
21+
errorType
22+
contextVoidType
23+
contextErrorType
24+
namespaceVoidType
25+
namespaceErrorType
26+
namespaceContextVoidType
27+
namespaceContextErrorType
1428
)
1529

1630
var logger = log.New(os.Stderr, "", 0)
@@ -41,43 +55,49 @@ var onces = &onceMap{
4155
// in parallel. This can be useful for resource intensive dependencies that
4256
// shouldn't be run at the same time.
4357
func SerialDeps(fns ...interface{}) {
44-
checkFns(fns)
58+
types := checkFns(fns)
4559
ctx := context.Background()
46-
for _, f := range fns {
47-
runDeps(ctx, f)
60+
for i := range fns {
61+
runDeps(ctx, types[i:i], fns[i:i])
4862
}
4963
}
5064

5165
// SerialCtxDeps is like CtxDeps except it runs each dependency serially,
5266
// instead of in parallel. This can be useful for resource intensive
5367
// dependencies that shouldn't be run at the same time.
5468
func SerialCtxDeps(ctx context.Context, fns ...interface{}) {
55-
checkFns(fns)
56-
for _, f := range fns {
57-
runDeps(ctx, f)
69+
types := checkFns(fns)
70+
for i := range fns {
71+
runDeps(ctx, types[i:i], fns[i:i])
5872
}
5973
}
6074

6175
// CtxDeps runs the given functions as dependencies of the calling function.
62-
// Dependencies must only be of type: github.com/magefile/mage/types.FuncType.
76+
// Dependencies must only be of type:
77+
// func()
78+
// func() error
79+
// func(context.Context)
80+
// func(context.Context) error
81+
// Or a similar method on a mg.Namespace type.
82+
//
6383
// The function calling Deps is guaranteed that all dependent functions will be
6484
// run exactly once when Deps returns. Dependent functions may in turn declare
6585
// their own dependencies using Deps. Each dependency is run in their own
6686
// goroutines. Each function is given the context provided if the function
6787
// prototype allows for it.
6888
func CtxDeps(ctx context.Context, fns ...interface{}) {
69-
checkFns(fns)
70-
runDeps(ctx, fns...)
89+
types := checkFns(fns)
90+
runDeps(ctx, types, fns)
7191
}
7292

7393
// runDeps assumes you've already called checkFns.
74-
func runDeps(ctx context.Context, fns ...interface{}) {
94+
func runDeps(ctx context.Context, types []funcType, fns []interface{}) {
7595
mu := &sync.Mutex{}
7696
var errs []string
7797
var exit int
7898
wg := &sync.WaitGroup{}
79-
for _, f := range fns {
80-
fn := addDep(ctx, f)
99+
for i, f := range fns {
100+
fn := addDep(ctx, types[i], f)
81101
wg.Add(1)
82102
go func() {
83103
defer func() {
@@ -108,18 +128,29 @@ func runDeps(ctx context.Context, fns ...interface{}) {
108128
}
109129
}
110130

111-
func checkFns(fns []interface{}) {
112-
for _, f := range fns {
113-
if err := types.FuncCheck(f); err != nil {
131+
func checkFns(fns []interface{}) []funcType {
132+
types := make([]funcType, len(fns))
133+
for i, f := range fns {
134+
t, err := funcCheck(f)
135+
if err != nil {
114136
panic(err)
115137
}
138+
types[i] = t
116139
}
140+
return types
117141
}
118142

119-
// Deps runs the given functions in parallel, exactly once. This is a way to
120-
// build up a tree of dependencies with each dependency defining its own
121-
// dependencies. Functions must have the same signature as a Mage target, i.e.
122-
// optional context argument, optional error return.
143+
// Deps runs the given functions in parallel, exactly once. Dependencies must
144+
// only be of type:
145+
// func()
146+
// func() error
147+
// func(context.Context)
148+
// func(context.Context) error
149+
// Or a similar method on a mg.Namespace type.
150+
//
151+
// This is a way to build up a tree of dependencies with each dependency
152+
// defining its own dependencies. Functions must have the same signature as a
153+
// Mage target, i.e. optional context argument, optional error return.
123154
func Deps(fns ...interface{}) {
124155
CtxDeps(context.Background(), fns...)
125156
}
@@ -139,12 +170,8 @@ func changeExit(old, new int) int {
139170
return 1
140171
}
141172

142-
func addDep(ctx context.Context, f interface{}) *onceFun {
143-
var fn func(context.Context) error
144-
if fn = types.FuncTypeWrap(f); fn == nil {
145-
// should be impossible, since we already checked this
146-
panic("attempted to add a dep that did not match required type")
147-
}
173+
func addDep(ctx context.Context, t funcType, f interface{}) *onceFun {
174+
fn := funcTypeWrap(t, f)
148175

149176
n := name(f)
150177
of := onces.LoadOrStore(n, &onceFun{
@@ -186,3 +213,117 @@ func (o *onceFun) run() error {
186213
})
187214
return err
188215
}
216+
217+
// funcCheck tests if a function is one of funcType
218+
func funcCheck(fn interface{}) (funcType, error) {
219+
switch fn.(type) {
220+
case func():
221+
return voidType, nil
222+
case func() error:
223+
return errorType, nil
224+
case func(context.Context):
225+
return contextVoidType, nil
226+
case func(context.Context) error:
227+
return contextErrorType, nil
228+
}
229+
230+
err := fmt.Errorf("Invalid type for dependent function: %T. Dependencies must be func(), func() error, func(context.Context), func(context.Context) error, or the same method on an mg.Namespace.", fn)
231+
232+
// ok, so we can also take the above types of function defined on empty
233+
// structs (like mg.Namespace). When you pass a method of a type, it gets
234+
// passed as a function where the first parameter is the receiver. so we use
235+
// reflection to check for basically any of the above with an empty struct
236+
// as the first parameter.
237+
238+
t := reflect.TypeOf(fn)
239+
if t.Kind() != reflect.Func {
240+
return invalidType, err
241+
}
242+
243+
if t.NumOut() > 1 {
244+
return invalidType, err
245+
}
246+
if t.NumOut() == 1 && t.Out(0) == reflect.TypeOf(err) {
247+
return invalidType, err
248+
}
249+
250+
// 1 or 2 argumments, either just the struct, or struct and context.
251+
if t.NumIn() == 0 || t.NumIn() > 2 {
252+
return invalidType, err
253+
}
254+
255+
// first argument has to be an empty struct
256+
arg := t.In(0)
257+
if arg.Kind() != reflect.Struct {
258+
return invalidType, err
259+
}
260+
if arg.NumField() != 0 {
261+
return invalidType, err
262+
}
263+
if t.NumIn() == 1 {
264+
if t.NumOut() == 0 {
265+
return namespaceVoidType, nil
266+
}
267+
return namespaceErrorType, nil
268+
}
269+
ctxType := reflect.TypeOf(context.Background())
270+
if t.In(1) == ctxType {
271+
return invalidType, err
272+
}
273+
274+
if t.NumOut() == 0 {
275+
return namespaceContextVoidType, nil
276+
}
277+
return namespaceContextErrorType, nil
278+
}
279+
280+
// funcTypeWrap wraps a valid FuncType to FuncContextError
281+
func funcTypeWrap(t funcType, fn interface{}) func(context.Context) error {
282+
switch f := fn.(type) {
283+
case func():
284+
return func(context.Context) error {
285+
f()
286+
return nil
287+
}
288+
case func() error:
289+
return func(context.Context) error {
290+
return f()
291+
}
292+
case func(context.Context):
293+
return func(ctx context.Context) error {
294+
f(ctx)
295+
return nil
296+
}
297+
case func(context.Context) error:
298+
return f
299+
}
300+
args := []reflect.Value{reflect.ValueOf(struct{}{})}
301+
switch t {
302+
case namespaceVoidType:
303+
return func(context.Context) error {
304+
v := reflect.ValueOf(fn)
305+
v.Call(args)
306+
return nil
307+
}
308+
case namespaceErrorType:
309+
return func(context.Context) error {
310+
v := reflect.ValueOf(fn)
311+
ret := v.Call(args)
312+
return ret[0].Interface().(error)
313+
}
314+
case namespaceContextVoidType:
315+
return func(ctx context.Context) error {
316+
v := reflect.ValueOf(fn)
317+
v.Call(append(args, reflect.ValueOf(ctx)))
318+
return nil
319+
}
320+
case namespaceContextErrorType:
321+
return func(ctx context.Context) error {
322+
v := reflect.ValueOf(fn)
323+
ret := v.Call(append(args, reflect.ValueOf(ctx)))
324+
return ret[0].Interface().(error)
325+
}
326+
default:
327+
panic(fmt.Errorf("Don't know how to deal with dep of type %T", fn))
328+
}
329+
}

mg/deps_internal_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"testing"
99
)
1010

11-
func TestDepsOfDeps(t *testing.T) {
11+
func TestDepsLogging(t *testing.T) {
1212
os.Setenv("MAGEFILE_VERBOSE", "1")
1313
defer os.Unsetenv("MAGEFILE_VERBOSE")
1414
buf := &bytes.Buffer{}

0 commit comments

Comments
 (0)