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