cuelang.org/go@v0.13.0/cue/load/instances.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 "context" 19 "fmt" 20 "io/fs" 21 "maps" 22 "path" 23 "slices" 24 "strconv" 25 "strings" 26 27 "cuelang.org/go/cue/ast" 28 "cuelang.org/go/cue/build" 29 "cuelang.org/go/internal/filetypes" 30 "cuelang.org/go/internal/mod/modimports" 31 "cuelang.org/go/internal/mod/modload" 32 "cuelang.org/go/internal/mod/modpkgload" 33 "cuelang.org/go/internal/mod/modrequirements" 34 "cuelang.org/go/internal/mod/semver" 35 "cuelang.org/go/mod/modfile" 36 "cuelang.org/go/mod/module" 37 ) 38 39 // Instances returns the instances named by the command line arguments 'args'. 40 // If errors occur trying to load an instance it is returned with Incomplete 41 // set. Errors directly related to loading the instance are recorded in this 42 // instance, but errors that occur loading dependencies are recorded in these 43 // dependencies. 44 func Instances(args []string, c *Config) []*build.Instance { 45 if len(args) == 0 { 46 args = []string{"."} 47 } 48 // TODO: This requires packages to be placed before files. At some point this 49 // could be relaxed. 50 i := 0 51 isAbsPkg := false 52 for ; i < len(args) && filetypes.IsPackage(args[i]); i++ { 53 if isAbsVersionPackage(args[i]) { 54 if i > 0 { 55 return []*build.Instance{c.newErrInstance(fmt.Errorf("only a single package with absolute version may be specified"))} 56 } 57 isAbsPkg = true 58 } 59 } 60 pkgArgs := args[:i] 61 otherArgs := args[i:] 62 otherFiles, err := filetypes.ParseArgs(otherArgs) 63 if err != nil { 64 return []*build.Instance{c.newErrInstance(err)} 65 } 66 ctx := context.TODO() 67 if c == nil { 68 c = &Config{} 69 } 70 newC, err := c.complete() 71 if err != nil { 72 return []*build.Instance{c.newErrInstance(err)} 73 } 74 c = newC 75 for _, f := range otherFiles { 76 if err := setFileSource(c, f); err != nil { 77 return []*build.Instance{c.newErrInstance(err)} 78 } 79 } 80 if c.Package != "" && c.Package != "_" && c.Package != "*" { 81 // The caller has specified an explicit package to load. 82 // This is essentially the same as passing an explicit package 83 // qualifier to all package arguments that don't already have 84 // one. We add that qualifier here so that there's a distinction 85 // between package paths specified as arguments, which 86 // have the qualifier added, and package paths that are dependencies 87 // of those, which don't. 88 pkgArgs1 := make([]string, 0, len(pkgArgs)) 89 for _, p := range pkgArgs { 90 if ip := ast.ParseImportPath(p); !ip.ExplicitQualifier { 91 ip.Qualifier = c.Package 92 p = ip.String() 93 } 94 pkgArgs1 = append(pkgArgs1, p) 95 } 96 pkgArgs = pkgArgs1 97 } 98 99 tg := newTagger(c) 100 101 var pkgs *modpkgload.Packages 102 if !c.SkipImports { 103 if isAbsPkg { 104 // Note: replace the absolute package (which isn't actually a valid 105 // import path and may contain a version query like @latest) 106 // with the actual resolved import path. 107 pkgArgs[0], pkgs, err = loadAbsPackage(ctx, c, pkgArgs[0], tg) 108 } else { 109 // Pass all arguments that look like packages to loadPackages 110 // so that they'll be available when looking up the packages 111 // that are specified on the command line. 112 expandedPaths, err1 := expandPackageArgs(c, pkgArgs, c.Package, tg) 113 if err1 != nil { 114 return []*build.Instance{c.newErrInstance(err1)} 115 } 116 pkgs, err = loadPackagesFromArgs(ctx, c, expandedPaths, otherFiles, tg) 117 } 118 if err != nil { 119 return []*build.Instance{c.newErrInstance(err)} 120 } 121 } 122 l := newLoader(c, tg, pkgs) 123 124 if c.Context == nil { 125 opts := []build.Option{ 126 build.ParseFile(c.ParseFile), 127 } 128 if f := l.loadFunc(); l != nil { 129 opts = append(opts, build.Loader(f)) 130 } 131 c.Context = build.NewContext(opts...) 132 } 133 134 a := []*build.Instance{} 135 if len(pkgArgs) > 0 { 136 for _, m := range l.importPaths(pkgArgs) { 137 if m.Err != nil { 138 inst := c.newErrInstance(m.Err) 139 a = append(a, inst) 140 continue 141 } 142 a = append(a, m.Pkgs...) 143 } 144 } 145 146 if len(otherFiles) > 0 { 147 a = append(a, l.cueFilesPackage(otherFiles)) 148 } 149 150 for _, p := range a { 151 tags, err := findTags(p) 152 if err != nil { 153 p.ReportError(err) 154 } 155 tg.tags = append(tg.tags, tags...) 156 } 157 158 // TODO(api): have API call that returns an error which is the aggregate 159 // of all build errors. Certain errors, like these, hold across builds. 160 if err := tg.injectTags(c.Tags); err != nil { 161 for _, p := range a { 162 p.ReportError(err) 163 } 164 return a 165 } 166 167 if tg.replacements == nil { 168 return a 169 } 170 171 for _, p := range a { 172 for _, f := range p.Files { 173 ast.Walk(f, nil, func(n ast.Node) { 174 if ident, ok := n.(*ast.Ident); ok { 175 if v, ok := tg.replacements[ident.Node]; ok { 176 ident.Node = v 177 } 178 } 179 }) 180 } 181 } 182 183 return a 184 } 185 186 // loadAbsPackage loads a single $package@$version package 187 // as the main module and returns its actual import path 188 // and the packages instance representing its module. 189 func loadAbsPackage( 190 ctx context.Context, 191 cfg *Config, 192 pkg string, 193 tg *tagger, 194 ) (string, *modpkgload.Packages, error) { 195 // First find the module that contains the package. 196 mv, _, err := modload.ResolveAbsolutePackage(ctx, cfg.Registry, pkg) 197 if err != nil { 198 return "", nil, err 199 } 200 // ResolveAbsolutePackage should already have fetched the module 201 // so this should be quick. 202 loc, err := cfg.Registry.Fetch(ctx, mv) 203 if err != nil { 204 return "", nil, err 205 } 206 modFilePath := path.Join(loc.Dir, modDir, moduleFile) 207 modFileData, err := fs.ReadFile(loc.FS, modFilePath) 208 if err != nil { 209 return "", nil, err 210 } 211 mf, err := modfile.Parse(modFileData, modFilePath) 212 if err != nil { 213 return "", nil, err 214 } 215 // Make the package path into a regular import path 216 // with only the major version suffix. 217 ip := ast.ParseImportPath(pkg) 218 ip.Version = semver.Major(mv.Version()) 219 220 pkgs, err := loadPackages(ctx, cfg, mf, loc, []string{ip.String()}, tg) 221 if err != nil { 222 return "", nil, err 223 } 224 return ip.String(), pkgs, nil 225 } 226 227 // loadPackages returns packages loaded from the given package list and also 228 // including imports from the given build files. 229 func loadPackagesFromArgs( 230 ctx context.Context, 231 cfg *Config, 232 pkgs []resolvedPackageArg, 233 otherFiles []*build.File, 234 tg *tagger, 235 ) (*modpkgload.Packages, error) { 236 if cfg.modFile == nil || cfg.modFile.Module == "" { 237 return nil, nil 238 } 239 pkgPaths := make(map[string]bool) 240 // Add any packages specified directly on the command line. 241 for _, pkg := range pkgs { 242 pkgPaths[pkg.resolvedCanonical] = true 243 } 244 // Add any imports found in other files. 245 for _, f := range otherFiles { 246 if f.Encoding != build.CUE { 247 // not a CUE file; assume it has no imports for now. 248 continue 249 } 250 syntax, err := cfg.fileSystem.getCUESyntax(f) 251 if err != nil { 252 return nil, fmt.Errorf("cannot get syntax for %q: %w", f.Filename, err) 253 } 254 for _, imp := range syntax.Imports { 255 pkgPath, err := strconv.Unquote(imp.Path.Value) 256 if err != nil { 257 // Should never happen. 258 return nil, fmt.Errorf("invalid import path %q in %s", imp.Path.Value, f.Filename) 259 } 260 // Canonicalize the path. 261 pkgPath = ast.ParseImportPath(pkgPath).Canonical().String() 262 pkgPaths[pkgPath] = true 263 } 264 } 265 return loadPackages(ctx, cfg, cfg.modFile, 266 module.SourceLoc{ 267 FS: cfg.fileSystem.ioFS(cfg.ModuleRoot), 268 Dir: ".", 269 }, 270 slices.Sorted(maps.Keys(pkgPaths)), 271 tg, 272 ) 273 } 274 275 func loadPackages( 276 ctx context.Context, 277 cfg *Config, 278 mainMod *modfile.File, 279 mainModLoc module.SourceLoc, 280 pkgPaths []string, 281 tg *tagger, 282 ) (*modpkgload.Packages, error) { 283 mainModPath := mainMod.QualifiedModule() 284 reqs := modrequirements.NewRequirements( 285 mainModPath, 286 cfg.Registry, 287 mainMod.DepVersions(), 288 mainMod.DefaultMajorVersions(), 289 ) 290 return modpkgload.LoadPackages( 291 ctx, 292 mainModPath, 293 mainModLoc, 294 reqs, 295 cfg.Registry, 296 pkgPaths, 297 func(pkgPath string, mod module.Version, fsys fs.FS, mf modimports.ModuleFile) bool { 298 if !cfg.Tools && strings.HasSuffix(mf.FilePath, "_tool.cue") { 299 return false 300 } 301 isTest := strings.HasSuffix(mf.FilePath, "_test.cue") 302 var tagIsSet func(string) bool 303 if mod.Path() == mainModPath { 304 // In the main module. 305 if isTest && !cfg.Tests { 306 return false 307 } 308 tagIsSet = tg.tagIsSet 309 } else { 310 // Outside the main module. 311 if isTest { 312 // Don't traverse test files outside the main module 313 return false 314 } 315 // Treat all build tag keys as unset. 316 tagIsSet = func(string) bool { 317 return false 318 } 319 } 320 if err := shouldBuildFile(mf.Syntax, tagIsSet); err != nil { 321 // Later build logic should pick up and report the same error. 322 return false 323 } 324 return true 325 }, 326 ), nil 327 } 328 329 func isAbsVersionPackage(p string) bool { 330 ip := ast.ParseImportPath(p) 331 if ip.Version == "" { 332 return false 333 } 334 if semver.Major(ip.Version) == ip.Version { 335 return false 336 } 337 // Anything other than a simple major version suffix counts 338 // as an absolute version. 339 return true 340 }