Skip to content

Commit b154b4e

Browse files
switchupcbldez
authored andcommitted
feat: support for Go modules to imports
1 parent dc082b5 commit b154b4e

File tree

7 files changed

+103
-178
lines changed

7 files changed

+103
-178
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ It powers executable Go scripts and plugins, in embedded interpreters or interac
1515
* Complete support of [Go specification][specs]
1616
* Written in pure Go, using only the standard library
1717
* Simple interpreter API: `New()`, `Eval()`, `Use()`
18+
* Supports Go Modules
1819
* Works everywhere Go works
1920
* All Go & runtime resources accessible from script (with control)
2021
* Security: `unsafe` and `syscall` packages neither used nor exported by default
@@ -95,6 +96,11 @@ func Bar(s string) string { return s + "-Foo" }`
9596
func main() {
9697
i := interp.New(interp.Options{})
9798

99+
// import the standard library (Go Modules)
100+
if err := i.Use(stdlib.Symbols); err != nil {
101+
t.Fatal(err)
102+
}
103+
98104
_, err := i.Eval(src)
99105
if err != nil {
100106
panic(err)

go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
module github.com/traefik/yaegi
22

33
go 1.18
4+
5+
require golang.org/x/tools v0.1.12
6+
7+
require (
8+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
9+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
10+
)

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
2+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
3+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
4+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
5+
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
6+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

interp/doc.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ Importing packages
99
Packages can be imported in source or binary form, using the standard
1010
Go import statement. In source form, packages are searched first in the
1111
vendor directory, the preferred way to store source dependencies. If not
12-
found in vendor, sources modules will be searched in GOPATH. Go modules
13-
are not supported yet by yaegi.
12+
found in vendor, sources modules will be searched in GOPATH.
1413
1514
Binary form packages are compiled and linked with the interpreter
1615
executable, and exposed to scripts with the Use method. The extract

interp/interp.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ type opt struct {
174174
// dotCmd is the command to process the dot graph produced when astDot and/or
175175
// cfgDot is enabled. It defaults to 'dot -Tdot -o <filename>.dot'.
176176
dotCmd string
177+
noRun bool // compile, but do not run
178+
fastChan bool // disable cancellable chan operations
177179
context build.Context // build context: GOPATH, build constraints
178180
stdin io.Reader // standard input
179181
stdout io.Writer // standard output
@@ -183,8 +185,6 @@ type opt struct {
183185
filesystem fs.FS // filesystem containing sources
184186
astDot bool // display AST graph (debug)
185187
cfgDot bool // display CFG graph (debug)
186-
noRun bool // compile, but do not run
187-
fastChan bool // disable cancellable chan operations
188188
specialStdio bool // allows os.Stdin, os.Stdout, os.Stderr to not be file descriptors
189189
unrestricted bool // allow use of non sandboxed symbols
190190
}
@@ -301,6 +301,12 @@ type Options struct {
301301
// GoPath sets GOPATH for the interpreter.
302302
GoPath string
303303

304+
// GoCache sets GOCACHE for the interpreter.
305+
GoCache string
306+
307+
// GoToolDir sets the GOTOOLDIR for the interpreter.
308+
GoToolDir string
309+
304310
// BuildTags sets build constraints for the interpreter.
305311
BuildTags []string
306312

@@ -328,7 +334,12 @@ type Options struct {
328334
// New returns a new interpreter.
329335
func New(options Options) *Interpreter {
330336
i := Interpreter{
331-
opt: opt{context: build.Default, filesystem: &realFS{}, env: map[string]string{}},
337+
opt: opt{
338+
context: build.Default, filesystem: &realFS{}, env: map[string]string{
339+
"goCache": options.GoCache,
340+
"goToolDir": options.GoToolDir,
341+
},
342+
},
332343
frame: newFrame(nil, 0, 0),
333344
fset: token.NewFileSet(),
334345
universe: initUniverse(),

interp/src.go

Lines changed: 66 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ package interp
33
import (
44
"fmt"
55
"io/fs"
6-
"os"
76
"path/filepath"
87
"strings"
8+
9+
"golang.org/x/tools/go/packages"
910
)
1011

11-
// importSrc calls gta on the source code for the package identified by
12+
// importSrc calls global tag analysis on the source code for the package identified by
1213
// importPath. rPath is the relative path to the directory containing the source
1314
// code for the package. It can also be "main" as a special value.
1415
func (interp *Interpreter) importSrc(rPath, importPath string, skipTest bool) (string, error) {
@@ -23,24 +24,14 @@ func (interp *Interpreter) importSrc(rPath, importPath string, skipTest bool) (s
2324
return name, nil
2425
}
2526

26-
// For relative import paths in the form "./xxx" or "../xxx", the initial
27-
// base path is the directory of the interpreter input file, or "." if no file
28-
// was provided.
29-
// In all other cases, absolute import paths are resolved from the GOPATH
30-
// and the nested "vendor" directories.
27+
// resolve relative and absolute import paths
3128
if isPathRelative(importPath) {
3229
if rPath == mainID {
3330
rPath = "."
3431
}
3532
dir = filepath.Join(filepath.Dir(interp.name), rPath, importPath)
36-
} else if dir, rPath, err = interp.pkgDir(interp.context.GOPATH, rPath, importPath); err != nil {
37-
// Try again, assuming a root dir at the source location.
38-
if rPath, err = interp.rootFromSourceLocation(); err != nil {
39-
return "", err
40-
}
41-
if dir, rPath, err = interp.pkgDir(interp.context.GOPATH, rPath, importPath); err != nil {
42-
return "", err
43-
}
33+
} else if dir, err = interp.getPackageDir(importPath); err != nil {
34+
return "", err
4435
}
4536

4637
if interp.rdir[importPath] {
@@ -171,119 +162,87 @@ func (interp *Interpreter) importSrc(rPath, importPath string, skipTest bool) (s
171162
return pkgName, nil
172163
}
173164

174-
// rootFromSourceLocation returns the path to the directory containing the input
175-
// Go file given to the interpreter, relative to $GOPATH/src.
176-
// It is meant to be called in the case when the initial input is a main package.
177-
func (interp *Interpreter) rootFromSourceLocation() (string, error) {
178-
sourceFile := interp.name
179-
if sourceFile == DefaultSourceName {
180-
return "", nil
181-
}
182-
wd, err := os.Getwd()
165+
// getPackageDir uses the GOPATH to find the absolute path of an import path.
166+
func (interp *Interpreter) getPackageDir(importPath string) (string, error) {
167+
// search the standard library and Go modules.
168+
config := packages.Config{}
169+
config.Env = append(config.Env, "GOPATH="+interp.context.GOPATH, "GOCACHE="+interp.opt.env["goCache"], "GOTOOLDIR="+interp.opt.env["goToolDir"])
170+
pkgs, err := packages.Load(&config, importPath)
183171
if err != nil {
184-
return "", err
172+
return "", fmt.Errorf("an error occurred retrieving a package from the GOPATH: %v\n%v\nIf Access is denied, run in administrator", importPath, err)
185173
}
186-
pkgDir := filepath.Join(wd, filepath.Dir(sourceFile))
187-
root := strings.TrimPrefix(pkgDir, filepath.Join(interp.context.GOPATH, "src")+"/")
188-
if root == wd {
189-
return "", fmt.Errorf("package location %s not in GOPATH", pkgDir)
174+
175+
// confirm the import path is found.
176+
for _, pkg := range pkgs {
177+
for _, goFile := range pkg.GoFiles {
178+
if strings.Contains(filepath.Dir(goFile), pkg.Name) {
179+
return filepath.Dir(goFile), nil
180+
}
181+
}
190182
}
191-
return root, nil
192-
}
193183

194-
// pkgDir returns the absolute path in filesystem for a package given its import path
195-
// and the root of the subtree dependencies.
196-
func (interp *Interpreter) pkgDir(goPath string, root, importPath string) (string, string, error) {
197-
rPath := filepath.Join(root, "vendor")
198-
dir := filepath.Join(goPath, "src", rPath, importPath)
184+
// check for certain go tools located in GOTOOLDIR.
185+
if interp.opt.env["goToolDir"] != "" {
186+
// search for the go directory before searching for packages.
187+
// this approach prevents the computer from searching the entire filesystem.
188+
godir, err := searchUpDirPath(interp.opt.env["goToolDir"], "go", false)
189+
if err != nil {
190+
return "", fmt.Errorf("an import source could not be found: %q\nThe current GOPATH=%v, GOCACHE=%v, GOTOOLDIR=%v\n%v", importPath, interp.context.GOPATH, interp.opt.env["goCache"], interp.opt.env["goToolDir"], err)
191+
}
199192

200-
if _, err := fs.Stat(interp.opt.filesystem, dir); err == nil {
201-
return dir, rPath, nil // found!
193+
absimportpath, err := searchDirs(godir, importPath)
194+
if err != nil {
195+
return "", fmt.Errorf("an import source could not be found: %q\nThe current GOPATH=%v, GOCACHE=%v, GOTOOLDIR=%v\n%v", importPath, interp.context.GOPATH, interp.opt.env["goCache"], interp.opt.env["goToolDir"], err)
196+
}
197+
return absimportpath, nil
202198
}
203199

204-
dir = filepath.Join(goPath, "src", effectivePkg(root, importPath))
200+
return "", fmt.Errorf("an import source could not be found: %q. Set the GOPATH and/or GOTOOLDIR environment variable from Interpreter.Options", importPath)
201+
}
205202

206-
if _, err := fs.Stat(interp.opt.filesystem, dir); err == nil {
207-
return dir, root, nil // found!
203+
// searchUpDirPath searches up a directory path in order to find a target directory.
204+
func searchUpDirPath(initial string, target string, isCaseSensitive bool) (string, error) {
205+
// strings.Split always returns [:0] as filepath.Dir returns "." or the last directory.
206+
splitdir := strings.Split(filepath.Clean(initial), string(filepath.Separator))
207+
if len(splitdir) == 1 {
208+
return "", fmt.Errorf("the target directory %q is not within the path %q", target, initial)
208209
}
209210

210-
if len(root) == 0 {
211-
if interp.context.GOPATH == "" {
212-
return "", "", fmt.Errorf("unable to find source related to: %q. Either the GOPATH environment variable, or the Interpreter.Options.GoPath needs to be set", importPath)
213-
}
214-
return "", "", fmt.Errorf("unable to find source related to: %q", importPath)
211+
updir := splitdir[len(splitdir)-1]
212+
if !isCaseSensitive {
213+
updir = strings.ToLower(updir)
215214
}
216-
217-
rootPath := filepath.Join(goPath, "src", root)
218-
prevRoot, err := previousRoot(interp.opt.filesystem, rootPath, root)
219-
if err != nil {
220-
return "", "", err
215+
if updir == target {
216+
return initial, nil
221217
}
222-
223-
return interp.pkgDir(goPath, prevRoot, importPath)
218+
return searchUpDirPath(filepath.Dir(initial), target, isCaseSensitive)
224219
}
225220

226-
const vendor = "vendor"
227-
228-
// Find the previous source root (vendor > vendor > ... > GOPATH).
229-
func previousRoot(filesystem fs.FS, rootPath, root string) (string, error) {
230-
rootPath = filepath.Clean(rootPath)
231-
parent, final := filepath.Split(rootPath)
232-
parent = filepath.Clean(parent)
233-
234-
// TODO(mpl): maybe it works for the special case main, but can't be bothered for now.
235-
if root != mainID && final != vendor {
236-
root = strings.TrimSuffix(root, string(filepath.Separator))
237-
prefix := strings.TrimSuffix(strings.TrimSuffix(rootPath, root), string(filepath.Separator))
238-
239-
// look for the closest vendor in one of our direct ancestors, as it takes priority.
240-
var vendored string
241-
for {
242-
fi, err := fs.Stat(filesystem, filepath.Join(parent, vendor))
243-
if err == nil && fi.IsDir() {
244-
vendored = strings.TrimPrefix(strings.TrimPrefix(parent, prefix), string(filepath.Separator))
245-
break
246-
}
247-
if !os.IsNotExist(err) {
248-
return "", err
249-
}
250-
251-
// stop when we reach GOPATH/src/blah
252-
parent = filepath.Dir(parent)
253-
if parent == prefix {
254-
break
255-
}
221+
// searchDirs searches within a directory (and its subdirectories) in an attempt to find a filepath.
222+
func searchDirs(initial string, target string) (string, error) {
223+
absfilepath, err := filepath.Abs(initial)
224+
if err != nil {
225+
return "", err
226+
}
256227

257-
// just an additional failsafe, stop if we reach the filesystem root, or dot (if
258-
// we are dealing with relative paths).
259-
// TODO(mpl): It should probably be a critical error actually,
260-
// as we shouldn't have gone that high up in the tree.
261-
if parent == string(filepath.Separator) || parent == "." {
262-
break
228+
// find the go directory.
229+
var foundpath string
230+
filter := func(path string, d fs.DirEntry, err error) error {
231+
if d.IsDir() {
232+
if d.Name() == target {
233+
foundpath = path
263234
}
264235
}
265-
266-
if vendored != "" {
267-
return vendored, nil
268-
}
236+
return nil
269237
}
270-
271-
// TODO(mpl): the algorithm below might be redundant with the one above,
272-
// but keeping it for now. Investigate/simplify/remove later.
273-
splitRoot := strings.Split(root, string(filepath.Separator))
274-
var index int
275-
for i := len(splitRoot) - 1; i >= 0; i-- {
276-
if splitRoot[i] == "vendor" {
277-
index = i
278-
break
279-
}
238+
if err = filepath.WalkDir(absfilepath, filter); err != nil {
239+
return "", fmt.Errorf("An error occurred searching for a directory.\n%v", err)
280240
}
281241

282-
if index == 0 {
283-
return "", nil
242+
if foundpath != "" {
243+
return foundpath, nil
284244
}
285-
286-
return filepath.Join(splitRoot[:index]...), nil
245+
return "", fmt.Errorf("The target filepath %q is not within the path %q", target, initial)
287246
}
288247

289248
func effectivePkg(root, path string) string {

interp/src_test.go

Lines changed: 3 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ func Test_pkgDir(t *testing.T) {
183183
}
184184
}
185185

186-
dir, rPath, err := interp.pkgDir(goPath, test.root, test.path)
186+
dir, err := interp.getPackageDir(test.path)
187187
if err != nil {
188188
t.Fatal(err)
189189
}
@@ -192,71 +192,8 @@ func Test_pkgDir(t *testing.T) {
192192
t.Errorf("[dir] got: %s, want: %s", dir, test.expected.dir)
193193
}
194194

195-
if rPath != test.expected.rpath {
196-
t.Errorf(" [rpath] got: %s, want: %s", rPath, test.expected.rpath)
197-
}
198-
})
199-
}
200-
}
201-
202-
func Test_previousRoot(t *testing.T) {
203-
testCases := []struct {
204-
desc string
205-
root string
206-
rootPathSuffix string
207-
expected string
208-
}{
209-
{
210-
desc: "GOPATH",
211-
root: "github.com/foo/pkg/",
212-
expected: "",
213-
},
214-
{
215-
desc: "vendor level 1",
216-
root: "github.com/foo/pkg/vendor/guthib.com/traefik/fromage",
217-
expected: "github.com/foo/pkg",
218-
},
219-
{
220-
desc: "vendor level 2",
221-
root: "github.com/foo/pkg/vendor/guthib.com/traefik/fromage/vendor/guthib.com/traefik/fuu",
222-
expected: "github.com/foo/pkg/vendor/guthib.com/traefik/fromage",
223-
},
224-
{
225-
desc: "vendor is sibling",
226-
root: "github.com/foo/bar",
227-
rootPathSuffix: "testdata/src/github.com/foo/bar",
228-
expected: "github.com/foo",
229-
},
230-
{
231-
desc: "vendor is uncle",
232-
root: "github.com/foo/bar/baz",
233-
rootPathSuffix: "testdata/src/github.com/foo/bar/baz",
234-
expected: "github.com/foo",
235-
},
236-
}
237-
238-
for _, test := range testCases {
239-
test := test
240-
t.Run(test.desc, func(t *testing.T) {
241-
t.Parallel()
242-
243-
var rootPath string
244-
if test.rootPathSuffix != "" {
245-
wd, err := os.Getwd()
246-
if err != nil {
247-
t.Fatal(err)
248-
}
249-
rootPath = filepath.Join(wd, test.rootPathSuffix)
250-
} else {
251-
rootPath = vendor
252-
}
253-
p, err := previousRoot(&realFS{}, rootPath, test.root)
254-
if err != nil {
255-
t.Error(err)
256-
}
257-
258-
if p != test.expected {
259-
t.Errorf("got: %s, want: %s", p, test.expected)
195+
if test.root != test.expected.rpath {
196+
t.Errorf(" [rpath] got: %s, want: %s", test.root, test.expected.rpath)
260197
}
261198
})
262199
}

0 commit comments

Comments
 (0)