cuelang.org/go@v0.13.0/cue/load/import.go (about) 1 // Copyright 2018 The CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package load 16 17 import ( 18 "cmp" 19 "fmt" 20 "io" 21 "io/fs" 22 pathpkg "path" 23 "path/filepath" 24 "slices" 25 "strings" 26 27 "cuelang.org/go/cue/ast" 28 "cuelang.org/go/cue/build" 29 "cuelang.org/go/cue/errors" 30 "cuelang.org/go/cue/token" 31 "cuelang.org/go/internal/filetypes" 32 "cuelang.org/go/mod/module" 33 ) 34 35 // importPkg returns details about the CUE package named by the import path, 36 // interpreting local import paths relative to l.cfg.Dir. 37 // If the path is a local import path naming a package that can be imported 38 // using a standard import path, the returned package will set p.ImportPath 39 // to that path. 40 // 41 // In the directory and ancestor directories up to including one with a 42 // cue.mod file, all .cue files are considered part of the package except for: 43 // 44 // - files starting with _ or . (likely editor temporary files) 45 // - files with build constraints not satisfied by the context 46 // 47 // If an error occurs, importPkg sets the error in the returned instance, 48 // which then may contain partial information. 49 // 50 // pkgName indicates which packages to load. It supports the following 51 // values: 52 // 53 // "" the default package for the directory, if only one 54 // is present. 55 // _ anonymous files (which may be marked with _) 56 // * all packages 57 func (l *loader) importPkg(pos token.Pos, p *build.Instance) []*build.Instance { 58 retErr := func(errs errors.Error) []*build.Instance { 59 // XXX: move this loop to ReportError 60 for _, err := range errors.Errors(errs) { 61 p.ReportError(err) 62 } 63 return []*build.Instance{p} 64 } 65 66 for _, item := range l.stk { 67 if item == p.ImportPath { 68 return retErr(&PackageError{Message: errors.NewMessagef("package import cycle not allowed")}) 69 } 70 } 71 l.stk.Push(p.ImportPath) 72 defer l.stk.Pop() 73 74 cfg := l.cfg 75 ctxt := cfg.fileSystem 76 77 if p.Err != nil { 78 return []*build.Instance{p} 79 } 80 81 fp := newFileProcessor(cfg, p, l.tagger) 82 83 if p.PkgName == "" { 84 if l.cfg.Package == "*" { 85 fp.allPackages = true 86 p.PkgName = "_" 87 } else { 88 p.PkgName = l.cfg.Package 89 } 90 } 91 if p.PkgName != "" { 92 // If we have an explicit package name, we can ignore other packages. 93 fp.ignoreOther = true 94 } 95 96 var dirs [][2]string 97 genDir := GenPath(cfg.ModuleRoot) 98 if strings.HasPrefix(p.Dir, genDir) { 99 dirs = append(dirs, [2]string{genDir, p.Dir}) 100 // && p.PkgName != "_" 101 for _, sub := range []string{"pkg", "usr"} { 102 rel, err := filepath.Rel(genDir, p.Dir) 103 if err != nil { 104 // should not happen 105 return retErr(errors.Wrapf(err, token.NoPos, "invalid path")) 106 } 107 base := filepath.Join(cfg.ModuleRoot, modDir, sub) 108 dir := filepath.Join(base, rel) 109 dirs = append(dirs, [2]string{base, dir}) 110 } 111 } else { 112 dirs = append(dirs, [2]string{cfg.ModuleRoot, p.Dir}) 113 } 114 115 found := false 116 for _, d := range dirs { 117 info, err := ctxt.stat(d[1]) 118 if err == nil && info.IsDir() { 119 found = true 120 break 121 } 122 } 123 124 if !found { 125 return retErr( 126 &PackageError{ 127 Message: errors.NewMessagef("cannot find package %q", p.DisplayPath), 128 }) 129 } 130 131 // This algorithm assumes that multiple directories within cue.mod/*/ 132 // have the same module scope and that there are no invalid modules. 133 inModule := false // if pkg == "_" 134 for _, d := range dirs { 135 if l.cfg.findModRoot(d[1]) != "" { 136 inModule = true 137 break 138 } 139 } 140 141 // Walk the parent directories up to the module root to add their files as well, 142 // since a package foo/bar/baz inherits from parent packages foo/bar and foo. 143 // See https://cuelang.org/docs/concept/modules-packages-instances/#instances. 144 for _, d := range dirs { 145 dir := filepath.Clean(d[1]) 146 for { 147 sd, ok := l.dirCachedBuildFiles[dir] 148 if !ok { 149 sd = l.scanDir(dir) 150 l.dirCachedBuildFiles[dir] = sd 151 } 152 if err := sd.err; err != nil { 153 if errors.Is(err, fs.ErrNotExist) { 154 break 155 } 156 return retErr(errors.Wrapf(err, token.NoPos, "import failed reading dir %v", dir)) 157 } 158 for _, name := range sd.filenames { 159 file, err := filetypes.ParseFileAndType(name, "", filetypes.Input) 160 if err != nil { 161 p.UnknownFiles = append(p.UnknownFiles, &build.File{ 162 Filename: name, 163 ExcludeReason: errors.Newf(token.NoPos, "unknown filetype"), 164 }) 165 } else { 166 fp.add(dir, file, 0) 167 } 168 } 169 if p.PkgName == "" || !inModule || l.cfg.isModRoot(dir) || dir == d[0] { 170 break 171 } 172 173 // From now on we just ignore files that do not belong to the same 174 // package. 175 fp.ignoreOther = true 176 177 parent, _ := filepath.Split(dir) 178 parent = filepath.Clean(parent) 179 180 if parent == dir || len(parent) < len(d[0]) { 181 break 182 } 183 dir = parent 184 } 185 } 186 187 all := []*build.Instance{} 188 189 for _, p := range fp.pkgs { 190 impPath, err := addImportQualifier(importPath(p.ImportPath), p.PkgName) 191 p.ImportPath = string(impPath) 192 if err != nil { 193 p.ReportError(errors.Promote(err, "")) 194 } 195 196 if len(p.BuildFiles) == 0 && 197 len(p.IgnoredFiles) == 0 && 198 len(p.OrphanedFiles) == 0 && 199 len(p.InvalidFiles) == 0 && 200 len(p.UnknownFiles) == 0 { 201 // The package has no files in it. This can happen 202 // when the default package added in newFileProcessor 203 // doesn't have any associated files. 204 continue 205 } 206 all = append(all, p) 207 rewriteFiles(p, cfg.ModuleRoot, false) 208 if errs := fp.finalize(p); errs != nil { 209 p.ReportError(errs) 210 return all 211 } 212 213 l.addFiles(p) 214 _ = p.Complete() 215 } 216 slices.SortFunc(all, func(a, b *build.Instance) int { 217 // Instances may share the same directory but have different package names. 218 // Sort by directory first, then by package name. 219 if c := cmp.Compare(a.Dir, b.Dir); c != 0 { 220 return c 221 } 222 223 return cmp.Compare(a.PkgName, b.PkgName) 224 }) 225 return all 226 } 227 228 func (l *loader) scanDir(dir string) cachedDirFiles { 229 files, err := l.cfg.fileSystem.readDir(dir) 230 if err != nil { 231 return cachedDirFiles{ 232 err: err, 233 } 234 } 235 filenames := make([]string, 0, len(files)) 236 for _, f := range files { 237 if f.IsDir() { 238 continue 239 } 240 name := f.Name() 241 if name == "-" { 242 // The name "-" has a special significance to the file types 243 // logic, but only when specified directly on the command line. 244 // We don't want an actual file named "-" to have special 245 // significant, so avoid that by making sure we don't see a naked "-" 246 // even when a file named "-" is present in a directory. 247 name = "./-" 248 } 249 filenames = append(filenames, name) 250 } 251 return cachedDirFiles{ 252 filenames: filenames, 253 } 254 } 255 256 func setFileSource(cfg *Config, f *build.File) error { 257 if f.Source != nil { 258 return nil 259 } 260 fullPath := f.Filename 261 if fullPath == "-" { 262 b, err := io.ReadAll(cfg.stdin()) 263 if err != nil { 264 return errors.Newf(token.NoPos, "read stdin: %v", err) 265 } 266 f.Source = b 267 return nil 268 } 269 if !filepath.IsAbs(fullPath) { 270 fullPath = filepath.Join(cfg.Dir, fullPath) 271 // Ensure that encoding.NewDecoder will work correctly. 272 f.Filename = fullPath 273 } 274 if fi := cfg.fileSystem.getOverlay(fullPath); fi != nil { 275 if fi.file != nil { 276 f.Source = fi.file 277 } else { 278 f.Source = fi.contents 279 } 280 } 281 return nil 282 } 283 284 func (l *loader) loadFunc() build.LoadFunc { 285 if l.cfg.SkipImports { 286 return nil 287 } 288 return l._loadFunc 289 } 290 291 func (l *loader) _loadFunc(pos token.Pos, path string) *build.Instance { 292 impPath := importPath(path) 293 if isLocalImport(path) { 294 return l.cfg.newErrInstance(errors.Newf(pos, "relative import paths not allowed (%q)", path)) 295 } 296 297 if isStdlibPackage(path) { 298 // It looks like a builtin. 299 return nil 300 } 301 302 p := l.newInstance(pos, impPath) 303 _ = l.importPkg(pos, p) 304 return p 305 } 306 307 // newRelInstance returns a build instance from the given 308 // relative import path. 309 func (l *loader) newRelInstance(pos token.Pos, path, pkgName string) *build.Instance { 310 if !isLocalImport(path) { 311 panic(fmt.Errorf("non-relative import path %q passed to newRelInstance", path)) 312 } 313 314 p := l.cfg.Context.NewInstance(path, nil) 315 p.PkgName = pkgName 316 p.DisplayPath = filepath.ToSlash(path) 317 // p.ImportPath = string(dir) // compute unique ID. 318 p.Root = l.cfg.ModuleRoot 319 p.Module = l.cfg.Module 320 321 var err errors.Error 322 if path != cleanImport(path) { 323 err = errors.Append(err, l.errPkgf(nil, 324 "non-canonical import path: %q should be %q", path, pathpkg.Clean(path))) 325 } 326 327 dir := filepath.Join(l.cfg.Dir, filepath.FromSlash(path)) 328 if pkgPath, e := importPathFromAbsDir(l.cfg, dir, path); e != nil { 329 // Detect later to keep error messages consistent. 330 } else { 331 // Add package qualifier if the configuration requires it. 332 name := l.cfg.Package 333 switch name { 334 case "_", "*": 335 name = "" 336 } 337 pkgPath, e := addImportQualifier(pkgPath, name) 338 if e != nil { 339 // Detect later to keep error messages consistent. 340 } else { 341 p.ImportPath = string(pkgPath) 342 } 343 } 344 345 p.Dir = dir 346 347 if filepath.IsAbs(path) || strings.HasPrefix(path, "/") { 348 err = errors.Append(err, errors.Newf(pos, 349 "absolute import path %q not allowed", path)) 350 } 351 if err != nil { 352 p.Err = errors.Append(p.Err, err) 353 p.Incomplete = true 354 } 355 356 return p 357 } 358 359 func importPathFromAbsDir(c *Config, absDir string, origPath string) (importPath, error) { 360 if c.ModuleRoot == "" { 361 return "", fmt.Errorf("cannot determine import path for %q (root undefined)", origPath) 362 } 363 364 dir := filepath.Clean(absDir) 365 if !strings.HasPrefix(dir, c.ModuleRoot) { 366 return "", fmt.Errorf("cannot determine import path for %q (dir outside of root)", origPath) 367 } 368 369 pkg := filepath.ToSlash(dir[len(c.ModuleRoot):]) 370 switch { 371 case strings.HasPrefix(pkg, "/cue.mod/"): 372 pkg = pkg[len("/cue.mod/"):] 373 if pkg == "" { 374 return "", fmt.Errorf("invalid package %q (root of %s)", origPath, modDir) 375 } 376 377 case c.Module == "": 378 return "", fmt.Errorf("cannot determine import path for %q (no module)", origPath) 379 default: 380 impPath := ast.ParseImportPath(c.Module) 381 impPath.Path += pkg 382 impPath.Qualifier = "" 383 pkg = impPath.String() 384 } 385 return importPath(pkg), nil 386 } 387 388 func (l *loader) newInstance(pos token.Pos, p importPath) *build.Instance { 389 dir, modPath, err := l.absDirFromImportPath(pos, p) 390 i := l.cfg.Context.NewInstance(dir, nil) 391 i.Err = errors.Append(i.Err, err) 392 i.Dir = dir 393 394 parts := ast.ParseImportPath(string(p)) 395 i.PkgName = parts.Qualifier 396 if i.PkgName == "" { 397 i.Err = errors.Append(i.Err, l.errPkgf([]token.Pos{pos}, "cannot determine package name for %q; set it explicitly with ':'", p)) 398 } else if i.PkgName == "_" { 399 i.Err = errors.Append(i.Err, l.errPkgf([]token.Pos{pos}, "_ is not a valid import path qualifier in %q", p)) 400 } 401 i.DisplayPath = string(p) 402 i.ImportPath = string(p) 403 i.Root = l.cfg.ModuleRoot 404 i.Module = modPath 405 406 return i 407 } 408 409 // absDirFromImportPath converts a giving import path to an absolute directory 410 // and a package name. The root directory must be set. 411 // 412 // The returned directory may not exist. 413 func (l *loader) absDirFromImportPath(pos token.Pos, p importPath) (dir string, modPath string, _ errors.Error) { 414 dir, modPath, err := l.absDirFromImportPath1(pos, p) 415 if err != nil { 416 // Any error trying to determine the package location 417 // is a PackageError. 418 return "", "", l.errPkgf([]token.Pos{pos}, "%s", err.Error()) 419 } 420 return dir, modPath, nil 421 } 422 423 func (l *loader) absDirFromImportPath1(pos token.Pos, p importPath) (absDir string, modPath string, err error) { 424 if p == "" { 425 return "", "", fmt.Errorf("empty import path") 426 } 427 if l.cfg.ModuleRoot == "" { 428 return "", "", fmt.Errorf("cannot import %q (root undefined)", p) 429 } 430 if isStdlibPackage(string(p)) { 431 return "", "", fmt.Errorf("standard library import path %q cannot be imported as a CUE package", p) 432 } 433 if l.pkgs == nil { 434 return "", "", fmt.Errorf("imports are unavailable because there is no cue.mod/module.cue file") 435 } 436 // Extract the package name. 437 parts := ast.ParseImportPath(string(p)) 438 unqualified := parts.Unqualified().String() 439 // TODO predicate registry-aware lookup on module.cue-declared CUE version? 440 441 // Note: use the canonical form of the import path because 442 // that's the form passed to [modpkgload.LoadPackages] 443 // and hence it's available by that name via Pkg. 444 pkg := l.pkgs.Pkg(parts.Canonical().String()) 445 // TODO(mvdan): using "unqualified" for the errors below doesn't seem right, 446 // should we not be using either the original path or the canonical path? 447 // The unqualified import path should only be used for filepath.FromSlash further below. 448 if pkg == nil { 449 return "", "", fmt.Errorf("no dependency found for package %q", unqualified) 450 } 451 if err := pkg.Error(); err != nil { 452 return "", "", fmt.Errorf("cannot find package %q: %v", unqualified, err) 453 } 454 if mv := pkg.Mod(); mv.IsLocal() { 455 // It's a local package that's present inside one or both of the gen, usr or pkg 456 // directories. Even though modpkgload tells us exactly what those directories 457 // are, the rest of the cue/load logic expects only a single directory for now, 458 // so just use that. 459 absDir = filepath.Join(GenPath(l.cfg.ModuleRoot), parts.Path) 460 } else { 461 locs := pkg.Locations() 462 if len(locs) > 1 { 463 return "", "", fmt.Errorf("package %q unexpectedly found in multiple locations", unqualified) 464 } 465 if len(locs) == 0 { 466 return "", "", fmt.Errorf("no location found for package %q", unqualified) 467 } 468 var err error 469 absDir, err = absPathForSourceLoc(locs[0]) 470 if err != nil { 471 return "", "", fmt.Errorf("cannot determine source directory for package %q: %v", unqualified, err) 472 } 473 } 474 return absDir, pkg.Mod().Path(), nil 475 } 476 477 func absPathForSourceLoc(loc module.SourceLoc) (string, error) { 478 osfs, ok := loc.FS.(module.OSRootFS) 479 if !ok { 480 return "", fmt.Errorf("cannot get absolute path for FS of type %T", loc.FS) 481 } 482 osPath := osfs.OSRoot() 483 if osPath == "" { 484 return "", fmt.Errorf("cannot get absolute path for FS of type %T", loc.FS) 485 } 486 return filepath.Join(osPath, loc.Dir), nil 487 } 488 489 // isStdlibPackage reports whether pkgPath looks like 490 // an import from the standard library. 491 func isStdlibPackage(pkgPath string) bool { 492 firstElem, _, _ := strings.Cut(pkgPath, "/") 493 if firstElem == "" { 494 return false // absolute paths like "/foo/bar" 495 } 496 // Paths like ".foo/bar", "./foo/bar", or "foo.com/bar" are not standard library import paths. 497 return strings.IndexByte(firstElem, '.') == -1 498 }