cue/load: allow storing module in cue.mod

Analogous to the module in go.mod, but using
CUE syntax.

This is to be used for modules where CUE files
import within the module root but the module name
cannot be derived from the context.

Change-Id: I4befd7cecb33c980c478bbe13e49534642b1aaa6
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2370
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/load/config.go b/cue/load/config.go
index 46ff754..2da8efb 100644
--- a/cue/load/config.go
+++ b/cue/load/config.go
@@ -19,6 +19,7 @@
 	"path/filepath"
 	"runtime"
 
+	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/build"
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/token"
@@ -75,7 +76,14 @@
 
 	loader *loader
 
-	modRoot string // module root for package paths ("" if unknown)
+	// A Module is a collection of packages and instances that are within the
+	// directory hierarchy rooted at the module root. The module root can be
+	// marked with a cue.mod file.
+	ModuleRoot string
+
+	// Module specifies the module prefix. If not empty, this value must match
+	// the module field of an existing cue.mod file.
+	Module string
 
 	// cache specifies the package cache in which to look for packages.
 	cache string
@@ -173,13 +181,13 @@
 	// TODO: determine root on a package basis. Maybe we even need a
 	// pkgname.cue.mod
 	// Look to see if there is a cue.mod.
-	if c.modRoot == "" {
+	if c.ModuleRoot == "" {
 		abs, err := c.findRoot(c.Dir)
 		if err != nil {
 			// Not using modules: only consider the current directory.
-			c.modRoot = c.Dir
+			c.ModuleRoot = c.Dir
 		} else {
-			c.modRoot = abs
+			c.ModuleRoot = abs
 		}
 	}
 
@@ -193,6 +201,32 @@
 		c.cache = filepath.Join(home(), defaultDir)
 	}
 
+	// TODO: also make this work if run from outside the module?
+	switch {
+	case true:
+		mod := filepath.Join(c.ModuleRoot, modFile)
+		f, cerr := c.fileSystem.openFile(mod)
+		if cerr != nil {
+			break
+		}
+		var r cue.Runtime
+		inst, err := r.Parse(mod, f)
+		if err != nil {
+			return nil, errors.Wrapf(err, token.NoPos, "invalid cue.mod file")
+		}
+		prefix := inst.Lookup("module")
+		if prefix.IsValid() {
+			name, err := prefix.String()
+			if err != nil {
+				return nil, err
+			}
+			if c.Module == "" || c.Module != name {
+				return nil, errors.Newf(prefix.Pos(), "inconsistent modules: got %q, want %q", name, c.Module)
+			}
+			c.Module = name
+		}
+	}
+
 	return &c, nil
 }
 
diff --git a/cue/load/import.go b/cue/load/import.go
index 11f6a6e..9607252 100644
--- a/cue/load/import.go
+++ b/cue/load/import.go
@@ -81,6 +81,11 @@
 	p.DisplayPath = path
 
 	isLocal := isLocalImport(path)
+
+	if cfg.Module != "" && isLocal {
+		p.ImportPath = filepath.Join(cfg.Module, path)
+	}
+
 	var modDir string
 	// var modErr error
 	if !isLocal {
@@ -125,7 +130,7 @@
 			}
 		}
 
-		if rootFound || dir == p.Root || fp.pkg.PkgName == "" {
+		if rootFound || filepath.Clean(dir) == l.cfg.ModuleRoot || fp.pkg.PkgName == "" {
 			break
 		}
 
@@ -176,13 +181,19 @@
 				}
 				return nil
 			}
-			if cfg.modRoot == "" {
+			if cfg.ModuleRoot == "" {
 				i := cfg.newInstance(path)
 				report(i, l.errPkgf(nil,
 					"import %q not found in the pkg directory", path))
 				return i
 			}
-			return l.importPkg(pos, path, filepath.Join(cfg.modRoot, "pkg"))
+			root := cfg.ModuleRoot
+			if mod := cfg.Module; path == mod || strings.HasPrefix(path, mod+"/") {
+				path = path[len(mod)+1:]
+			} else {
+				root = filepath.Join(root, "pkg")
+			}
+			return l.importPkg(pos, path, root)
 		}
 
 		if strings.Contains(path, "@") {
diff --git a/cue/load/loader.go b/cue/load/loader.go
index 4753763..aa8e38b 100644
--- a/cue/load/loader.go
+++ b/cue/load/loader.go
@@ -42,7 +42,7 @@
 
 	c, err := c.complete()
 	if err != nil {
-		return nil
+		return []*build.Instance{c.newErrInstance(nil, "", err)}
 	}
 
 	l := c.loader
@@ -57,8 +57,7 @@
 	a := []*build.Instance{}
 	for _, m := range l.importPaths(args) {
 		if m.Err != nil {
-			inst := c.newErrInstance(m, "",
-				errors.Wrapf(m.Err, token.NoPos, "no match"))
+			inst := c.newErrInstance(m, "", m.Err)
 			a = append(a, inst)
 			continue
 		}
diff --git a/cue/load/search.go b/cue/load/search.go
index da43ad5..3f2d0de 100644
--- a/cue/load/search.go
+++ b/cue/load/search.go
@@ -148,11 +148,11 @@
 
 	root := l.abs(dir)
 
-	if c.modRoot != "" {
-		if !hasFilepathPrefix(root, c.modRoot) {
+	if c.ModuleRoot != "" {
+		if !hasFilepathPrefix(root, c.ModuleRoot) {
 			m.Err = errors.Newf(token.NoPos,
 				"cue: pattern %s refers to dir %s, outside module root %s",
-				pattern, root, c.modRoot)
+				pattern, root, c.ModuleRoot)
 			return m
 		}
 	}