github.com/gernest/nezuko@v0.1.2/internal/modload/import.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package modload 6 7 import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "go/build" 12 "os" 13 "path/filepath" 14 "sort" 15 "strings" 16 "time" 17 18 "github.com/gernest/nezuko/internal/cfg" 19 "github.com/gernest/nezuko/internal/modfetch" 20 "github.com/gernest/nezuko/internal/modfetch/codehost" 21 "github.com/gernest/nezuko/internal/module" 22 "github.com/gernest/nezuko/internal/par" 23 "github.com/gernest/nezuko/internal/semver" 24 ) 25 26 type ImportMissingError struct { 27 ImportPath string 28 Module module.Version 29 } 30 31 func (e *ImportMissingError) Error() string { 32 if e.Module.Path == "" { 33 return "cannot find module providing package " + e.ImportPath 34 } 35 return "missing module for import: " + e.Module.Path + "@" + e.Module.Version + " provides " + e.ImportPath 36 } 37 38 // Import finds the module and directory in the build list 39 // containing the package with the given import path. 40 // The answer must be unique: Import returns an error 41 // if multiple modules attempt to provide the same package. 42 // Import can return a module with an empty m.Path, for packages in the standard library. 43 // Import can return an empty directory string, for fake packages like "C" and "unsafe". 44 // 45 // If the package cannot be found in the current build list, 46 // Import returns an ImportMissingError as the error. 47 // If Import can identify a module that could be added to supply the package, 48 // the ImportMissingError records that module. 49 func Import(path string) (m module.Version, dir string, err error) { 50 if strings.Contains(path, "@") { 51 return module.Version{}, "", fmt.Errorf("import path should not have @version") 52 } 53 if build.IsLocalImport(path) { 54 return module.Version{}, "", fmt.Errorf("relative import not supported") 55 } 56 57 // -mod=vendor is special. 58 // Everything must be in the main module or the main module's vendor directory. 59 if cfg.BuildMod == "vendor" { 60 mainDir, mainOK := dirInModule(path, Target.Path, ModRoot(), true) 61 vendorDir, vendorOK := dirInModule(path, "", filepath.Join(ModRoot(), "vendor"), false) 62 if mainOK && vendorOK { 63 return module.Version{}, "", fmt.Errorf("ambiguous import: found %s in multiple directories:\n\t%s\n\t%s", path, mainDir, vendorDir) 64 } 65 // Prefer to return main directory if there is one, 66 // Note that we're not checking that the package exists. 67 // We'll leave that for load. 68 if !vendorOK && mainDir != "" { 69 return Target, mainDir, nil 70 } 71 readVendorList() 72 return vendorMap[path], vendorDir, nil 73 } 74 75 // Check each module on the build list. 76 var dirs []string 77 var mods []module.Version 78 for _, m := range buildList { 79 if !maybeInModule(path, m.Path) { 80 // Avoid possibly downloading irrelevant modules. 81 continue 82 } 83 root, isLocal, err := fetch(m) 84 if err != nil { 85 // Report fetch error. 86 // Note that we don't know for sure this module is necessary, 87 // but it certainly _could_ provide the package, and even if we 88 // continue the loop and find the package in some other module, 89 // we need to look at this module to make sure the import is 90 // not ambiguous. 91 return module.Version{}, "", err 92 } 93 dir, ok := dirInModule(path, m.Path, root, isLocal) 94 if ok { 95 mods = append(mods, m) 96 dirs = append(dirs, dir) 97 } 98 } 99 if len(mods) == 1 { 100 return mods[0], dirs[0], nil 101 } 102 if len(mods) > 0 { 103 var buf bytes.Buffer 104 fmt.Fprintf(&buf, "ambiguous import: found %s in multiple modules:", path) 105 for i, m := range mods { 106 fmt.Fprintf(&buf, "\n\t%s", m.Path) 107 if m.Version != "" { 108 fmt.Fprintf(&buf, " %s", m.Version) 109 } 110 fmt.Fprintf(&buf, " (%s)", dirs[i]) 111 } 112 return module.Version{}, "", errors.New(buf.String()) 113 } 114 115 // Look up module containing the package, for addition to the build list. 116 // Goal is to determine the module, download it to dir, and return m, dir, ErrMissing. 117 if cfg.BuildMod == "readonly" { 118 return module.Version{}, "", fmt.Errorf("import lookup disabled by -mod=%s", cfg.BuildMod) 119 } 120 121 // Not on build list. 122 // To avoid spurious remote fetches, next try the latest replacement for each module. 123 // (golang.org/issue/26241) 124 if modFile != nil { 125 latest := map[string]string{} // path -> version 126 for _, r := range modFile.Replace { 127 if maybeInModule(path, r.Old.Path) { 128 latest[r.Old.Path] = semver.Max(r.Old.Version, latest[r.Old.Path]) 129 } 130 } 131 132 mods = make([]module.Version, 0, len(latest)) 133 for p, v := range latest { 134 // If the replacement didn't specify a version, synthesize a 135 // pseudo-version with an appropriate major version and a timestamp below 136 // any real timestamp. That way, if the main module is used from within 137 // some other module, the user will be able to upgrade the requirement to 138 // any real version they choose. 139 if v == "" { 140 if _, pathMajor, ok := module.SplitPathVersion(p); ok && len(pathMajor) > 0 { 141 v = modfetch.PseudoVersion(pathMajor[1:], "", time.Time{}, "000000000000") 142 } else { 143 v = modfetch.PseudoVersion("v0", "", time.Time{}, "000000000000") 144 } 145 } 146 mods = append(mods, module.Version{Path: p, Version: v}) 147 } 148 149 // Every module path in mods is a prefix of the import path. 150 // As in QueryPackage, prefer the longest prefix that satisfies the import. 151 sort.Slice(mods, func(i, j int) bool { 152 return len(mods[i].Path) > len(mods[j].Path) 153 }) 154 for _, m := range mods { 155 root, isLocal, err := fetch(m) 156 if err != nil { 157 // Report fetch error as above. 158 return module.Version{}, "", err 159 } 160 _, ok := dirInModule(path, m.Path, root, isLocal) 161 if ok { 162 return m, "", &ImportMissingError{ImportPath: path, Module: m} 163 } 164 } 165 } 166 m, _, err = QueryPackage(path, "latest", Allowed) 167 if err != nil { 168 if _, ok := err.(*codehost.VCSError); ok { 169 return module.Version{}, "", err 170 } 171 return module.Version{}, "", &ImportMissingError{ImportPath: path} 172 } 173 return m, "", &ImportMissingError{ImportPath: path, Module: m} 174 } 175 176 // maybeInModule reports whether, syntactically, 177 // a package with the given import path could be supplied 178 // by a module with the given module path (mpath). 179 func maybeInModule(path, mpath string) bool { 180 return mpath == path || 181 len(path) > len(mpath) && path[len(mpath)] == '/' && path[:len(mpath)] == mpath 182 } 183 184 var haveGoModCache, haveGoFilesCache par.Cache 185 186 // dirInModule locates the directory that would hold the package named by the given path, 187 // if it were in the module with module path mpath and root mdir. 188 // If path is syntactically not within mpath, 189 // or if mdir is a local file tree (isLocal == true) and the directory 190 // that would hold path is in a sub-module (covered by a z.mod below mdir), 191 // dirInModule returns "", false. 192 // 193 // Otherwise, dirInModule returns the name of the directory where 194 // Go source files would be expected, along with a boolean indicating 195 // whether there are in fact Go source files in that directory. 196 func dirInModule(path, mpath, mdir string, isLocal bool) (dir string, haveGoFiles bool) { 197 // Determine where to expect the package. 198 if path == mpath { 199 dir = mdir 200 } else if mpath == "" { // vendor directory 201 dir = filepath.Join(mdir, path) 202 } else if len(path) > len(mpath) && path[len(mpath)] == '/' && path[:len(mpath)] == mpath { 203 dir = filepath.Join(mdir, path[len(mpath)+1:]) 204 } else { 205 return "", false 206 } 207 208 // Check that there aren't other modules in the way. 209 // This check is unnecessary inside the module cache 210 // and important to skip in the vendor directory, 211 // where all the module trees have been overlaid. 212 // So we only check local module trees 213 // (the main module, and any directory trees pointed at by replace directives). 214 if isLocal { 215 for d := dir; d != mdir && len(d) > len(mdir); { 216 haveGoMod := haveGoModCache.Do(d, func() interface{} { 217 _, err := os.Stat(filepath.Join(d, "z.mod")) 218 return err == nil 219 }).(bool) 220 221 if haveGoMod { 222 return "", false 223 } 224 parent := filepath.Dir(d) 225 if parent == d { 226 // Break the loop, as otherwise we'd loop 227 // forever if d=="." and mdir=="". 228 break 229 } 230 d = parent 231 } 232 } 233 234 // Now committed to returning dir (not ""). 235 236 // Are there Go source files in the directory? 237 // We don't care about build tags, not even "+build ignore". 238 // We're just looking for a plausible directory. 239 haveGoFiles = haveGoFilesCache.Do(dir, func() interface{} { 240 f, err := os.Open(dir) 241 if err != nil { 242 return false 243 } 244 defer f.Close() 245 names, _ := f.Readdirnames(-1) 246 for _, name := range names { 247 if strings.HasSuffix(name, ".zig") { 248 info, err := os.Stat(filepath.Join(dir, name)) 249 if err == nil && info.Mode().IsRegular() { 250 return true 251 } 252 } 253 } 254 return false 255 }).(bool) 256 257 return dir, haveGoFiles 258 }