cue/load: allow imports from a module root package directory

It is unclear whether we should have a module cache similar to Go.
For now we will use this. It is compatible with using a go get
(module style) approach later.

Change-Id: I5fa34f81c6e7c0baacbff38aa8cc6ea41d078324
Reviewed-on: https://cue-review.googlesource.com/c/1601
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/load/config.go b/cue/load/config.go
index 3e1633b..bf5f398 100644
--- a/cue/load/config.go
+++ b/cue/load/config.go
@@ -26,6 +26,7 @@
 	cueSuffix  = ".cue"
 	defaultDir = "cue"
 	modFile    = "cue.mod"
+	pkgDir     = "pkg" // TODO: vendor?
 )
 
 // FromArgsUsage is a partial usage message that applications calling
@@ -138,17 +139,6 @@
 		}
 	}
 
-	c.loader = &loader{cfg: &c}
-
-	if c.Context == nil {
-		c.Context = build.NewContext(build.Loader(c.loader.loadFunc(c.Dir)))
-	}
-
-	if c.cache == "" {
-		c.cache = filepath.Join(home(), defaultDir)
-		// os.MkdirAll(c.Cache, 0755) // TODO: tools task
-	}
-
 	// TODO: determine root on a package basis. Maybe we even need a
 	// pkgname.cue.mod
 	// Look to see if there is a cue.mod.
@@ -161,26 +151,50 @@
 			c.modRoot = abs
 		}
 	}
+
+	c.loader = &loader{cfg: &c}
+
+	if c.Context == nil {
+		c.Context = build.NewContext(build.Loader(c.loader.loadFunc(c.Dir)))
+	}
+
+	if c.cache == "" {
+		c.cache = filepath.Join(home(), defaultDir)
+		// os.MkdirAll(c.Cache, 0755) // TODO: tools task
+	}
+
 	return &c, nil
 }
 
 func findRoot(dir string) (string, error) {
-	abs, err := filepath.Abs(dir)
+	absDir, err := filepath.Abs(dir)
 	if err != nil {
 		return "", err
 	}
+	abs := absDir
 	for {
 		info, err := os.Stat(filepath.Join(abs, modFile))
 		if err == nil && !info.IsDir() {
-			break
+			return abs, nil
 		}
 		d := filepath.Dir(abs)
 		if len(d) >= len(abs) {
-			return "", err // reached top of file system, no cue.mod
+			break // reached top of file system, no cue.mod
 		}
 		abs = d
 	}
-	return abs, nil
+	abs = absDir
+	for {
+		info, err := os.Stat(filepath.Join(abs, pkgDir))
+		if err == nil && info.IsDir() {
+			return abs, nil
+		}
+		d := filepath.Dir(abs)
+		if len(d) >= len(abs) {
+			return "", err // reached top of file system, no pkg dir.
+		}
+		abs = d
+	}
 }
 
 func home() string {
diff --git a/cue/load/import.go b/cue/load/import.go
index 0d2d35c..7ad72ee 100644
--- a/cue/load/import.go
+++ b/cue/load/import.go
@@ -18,6 +18,7 @@
 	"bytes"
 	"fmt"
 	"log"
+	"os"
 	pathpkg "path"
 	"path/filepath"
 	"sort"
@@ -75,7 +76,7 @@
 
 	parentPath := path
 	if isLocalImport(path) {
-		parentPath = filepath.Join(srcDir, path)
+		parentPath = filepath.Join(srcDir, filepath.FromSlash(path))
 	}
 	p := cfg.Context.NewInstance(path, l.loadFunc(parentPath))
 	p.DisplayPath = path
@@ -162,10 +163,18 @@
 	return func(path string) *build.Instance {
 		cfg := l.cfg
 
-		// TODO: HACK: for now we don't handle any imports that are not
-		// relative paths.
 		if !isLocalImport(path) {
-			return nil
+			// is it a builtin?
+			if strings.IndexByte(strings.Split(path, "/")[0], '.') == -1 {
+				return nil
+			}
+			if cfg.modRoot == "" {
+				i := cfg.newInstance(path)
+				report(i, l.errPkgf(nil,
+					"import %q not found in the pkg directory", path))
+				return i
+			}
+			return l.importPkg(path, filepath.Join(cfg.modRoot, "pkg"))
 		}
 
 		if strings.Contains(path, "@") {
@@ -186,6 +195,10 @@
 		return fmt.Errorf("import %q: invalid import path", path)
 	}
 
+	if ctxt.isAbsPath(path) || strings.HasPrefix(path, "/") {
+		return fmt.Errorf("load: absolute import path %q not allowed", path)
+	}
+
 	if isLocalImport(path) {
 		if srcDir == "" {
 			return fmt.Errorf("import %q: import relative to unknown directory", path)
@@ -194,14 +207,15 @@
 			p.Dir = ctxt.joinPath(srcDir, path)
 		}
 		return nil
+	} else {
+		dir := ctxt.joinPath(srcDir, path)
+		info, err := os.Stat(filepath.Join(srcDir, path))
+		if err == nil && info.IsDir() {
+			p.Dir = dir
+			return nil
+		}
 	}
 
-	if strings.HasPrefix(path, "/") {
-		return fmt.Errorf("import %q: cannot import absolute path", path)
-	}
-
-	// TODO: Lookup the import in dir "pkg" at the module root.
-
 	// package was not found
 	return fmt.Errorf("cannot find package %q", path)
 }
diff --git a/cue/load/loader_test.go b/cue/load/loader_test.go
index 36effb6..5d79d41 100644
--- a/cue/load/loader_test.go
+++ b/cue/load/loader_test.go
@@ -77,6 +77,12 @@
 		args: args("./empty"),
 		want: ":  (0 files)",
 		err:  `no CUE files in ./empty`,
+	}, {
+		args: args("./imports"),
+		want: `
+imports: imports/imports.cue (1 files)
+	catch: pkg/acme.com/catch/catch.cue (1 files)`,
+		err: ``,
 	}}
 	for i, tc := range testCases {
 		t.Run(strconv.Itoa(i)+"/"+strings.Join(tc.args, ":"), func(t *testing.T) {
diff --git a/cue/load/testdata/imports/imports.cue b/cue/load/testdata/imports/imports.cue
new file mode 100644
index 0000000..8fbaa13
--- /dev/null
+++ b/cue/load/testdata/imports/imports.cue
@@ -0,0 +1,7 @@
+package imports
+
+import "acme.com/catch"
+
+coyoteTry1: catch.Method & "tnt"
+
+coyoteTry2: catch.Method & =~"cat"
diff --git a/cue/load/testdata/other/file/file.cue b/cue/load/testdata/other/file/file.cue
index 57dcc90..bd56fec 100644
--- a/cue/load/testdata/other/file/file.cue
+++ b/cue/load/testdata/other/file/file.cue
@@ -1,5 +1,3 @@
-// Test data - not compiled.
-
 package file
 
 {}
diff --git a/cue/load/testdata/pkg/acme.com/catch/catch.cue b/cue/load/testdata/pkg/acme.com/catch/catch.cue
new file mode 100644
index 0000000..7b952fe
--- /dev/null
+++ b/cue/load/testdata/pkg/acme.com/catch/catch.cue
@@ -0,0 +1,3 @@
+package catch
+
+Method: "tnt" | "catapult" | "net"