github.com/99designs/gqlgen@v0.17.45/internal/code/packages.go (about) 1 package code 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "runtime/debug" 11 "strings" 12 "sync" 13 14 "golang.org/x/tools/go/packages" 15 ) 16 17 var ( 18 once = sync.Once{} 19 modInfo *debug.BuildInfo 20 ) 21 22 var mode = packages.NeedName | 23 packages.NeedFiles | 24 packages.NeedImports | 25 packages.NeedTypes | 26 packages.NeedSyntax | 27 packages.NeedTypesInfo | 28 packages.NeedModule | 29 packages.NeedDeps 30 31 type ( 32 // Packages is a wrapper around x/tools/go/packages that maintains a (hopefully prewarmed) cache of packages 33 // that can be invalidated as writes are made and packages are known to change. 34 Packages struct { 35 packages map[string]*packages.Package 36 importToName map[string]string 37 loadErrors []error 38 buildFlags []string 39 40 numLoadCalls int // stupid test steam. ignore. 41 numNameCalls int // stupid test steam. ignore. 42 } 43 // Option is a function that can be passed to NewPackages to configure the package loader 44 Option func(p *Packages) 45 ) 46 47 // WithBuildTags adds build tags to the packages.Load call 48 func WithBuildTags(tags ...string) func(p *Packages) { 49 return func(p *Packages) { 50 p.buildFlags = append(p.buildFlags, "-tags", strings.Join(tags, ",")) 51 } 52 } 53 54 // NewPackages creates a new packages cache 55 // It will load all packages in the current module, and any packages that are passed to Load or LoadAll 56 func NewPackages(opts ...Option) *Packages { 57 p := &Packages{} 58 for _, opt := range opts { 59 opt(p) 60 } 61 return p 62 } 63 64 func (p *Packages) CleanupUserPackages() { 65 once.Do(func() { 66 var ok bool 67 modInfo, ok = debug.ReadBuildInfo() 68 if !ok { 69 modInfo = nil 70 } 71 }) 72 // Don't cleanup github.com/99designs/gqlgen prefixed packages, 73 // they haven't changed and do not need to be reloaded 74 if modInfo != nil { 75 var toRemove []string 76 for k := range p.packages { 77 if !strings.HasPrefix(k, modInfo.Main.Path) { 78 toRemove = append(toRemove, k) 79 } 80 } 81 for _, k := range toRemove { 82 delete(p.packages, k) 83 } 84 } else { 85 p.packages = nil // Cleanup all packages if we don't know for some reason which ones to keep 86 } 87 } 88 89 // ReloadAll will call LoadAll after clearing the package cache, so we can reload 90 // packages in the case that the packages have changed 91 func (p *Packages) ReloadAll(importPaths ...string) []*packages.Package { 92 if p.packages != nil { 93 p.CleanupUserPackages() 94 } 95 return p.LoadAll(importPaths...) 96 } 97 98 // LoadAll will call packages.Load and return the package data for the given packages, 99 // but if the package already have been loaded it will return cached values instead. 100 func (p *Packages) LoadAll(importPaths ...string) []*packages.Package { 101 if p.packages == nil { 102 p.packages = map[string]*packages.Package{} 103 } 104 105 missing := make([]string, 0, len(importPaths)) 106 for _, path := range importPaths { 107 if _, ok := p.packages[path]; ok { 108 continue 109 } 110 missing = append(missing, path) 111 } 112 113 if len(missing) > 0 { 114 p.numLoadCalls++ 115 pkgs, err := packages.Load(&packages.Config{ 116 Mode: mode, 117 BuildFlags: p.buildFlags, 118 }, missing...) 119 if err != nil { 120 p.loadErrors = append(p.loadErrors, err) 121 } 122 123 for _, pkg := range pkgs { 124 p.addToCache(pkg) 125 } 126 } 127 128 res := make([]*packages.Package, 0, len(importPaths)) 129 for _, path := range importPaths { 130 res = append(res, p.packages[NormalizeVendor(path)]) 131 } 132 return res 133 } 134 135 func (p *Packages) addToCache(pkg *packages.Package) { 136 imp := NormalizeVendor(pkg.PkgPath) 137 p.packages[imp] = pkg 138 for _, imp := range pkg.Imports { 139 if _, found := p.packages[NormalizeVendor(imp.PkgPath)]; !found { 140 p.addToCache(imp) 141 } 142 } 143 } 144 145 // Load works the same as LoadAll, except a single package at a time. 146 func (p *Packages) Load(importPath string) *packages.Package { 147 // Quick cache check first to avoid expensive allocations of LoadAll() 148 if p.packages != nil { 149 if pkg, ok := p.packages[importPath]; ok { 150 return pkg 151 } 152 } 153 154 pkgs := p.LoadAll(importPath) 155 if len(pkgs) == 0 { 156 return nil 157 } 158 return pkgs[0] 159 } 160 161 // LoadWithTypes tries a standard load, which may not have enough type info (TypesInfo== nil) available if the imported package is a 162 // second order dependency. Fortunately this doesnt happen very often, so we can just issue a load when we detect it. 163 func (p *Packages) LoadWithTypes(importPath string) *packages.Package { 164 pkg := p.Load(importPath) 165 if pkg == nil || pkg.TypesInfo == nil { 166 p.numLoadCalls++ 167 pkgs, err := packages.Load(&packages.Config{ 168 Mode: mode, 169 BuildFlags: p.buildFlags, 170 }, importPath) 171 if err != nil { 172 p.loadErrors = append(p.loadErrors, err) 173 return nil 174 } 175 p.addToCache(pkgs[0]) 176 pkg = pkgs[0] 177 } 178 return pkg 179 } 180 181 // NameForPackage looks up the package name from the package stanza in the go files at the given import path. 182 func (p *Packages) NameForPackage(importPath string) string { 183 if importPath == "" { 184 panic(errors.New("import path can not be empty")) 185 } 186 if p.importToName == nil { 187 p.importToName = map[string]string{} 188 } 189 190 importPath = NormalizeVendor(importPath) 191 192 // if its in the name cache use it 193 if name := p.importToName[importPath]; name != "" { 194 return name 195 } 196 197 // otherwise we might have already loaded the full package data for it cached 198 pkg := p.packages[importPath] 199 200 if pkg == nil { 201 // otherwise do a name only lookup for it but don't put it in the package cache. 202 p.numNameCalls++ 203 pkgs, err := packages.Load(&packages.Config{ 204 Mode: packages.NeedName, 205 BuildFlags: p.buildFlags, 206 }, importPath) 207 if err != nil { 208 p.loadErrors = append(p.loadErrors, err) 209 } else { 210 pkg = pkgs[0] 211 } 212 } 213 214 if pkg == nil || pkg.Name == "" { 215 return SanitizePackageName(filepath.Base(importPath)) 216 } 217 218 p.importToName[importPath] = pkg.Name 219 220 return pkg.Name 221 } 222 223 // Evict removes a given package import path from the cache, along with any packages that depend on it. Further calls 224 // to Load will fetch it from disk. 225 func (p *Packages) Evict(importPath string) { 226 delete(p.packages, importPath) 227 228 for _, pkg := range p.packages { 229 for _, imported := range pkg.Imports { 230 if imported.PkgPath == importPath { 231 p.Evict(pkg.PkgPath) 232 } 233 } 234 } 235 } 236 237 func (p *Packages) ModTidy() error { 238 p.packages = nil 239 tidyCmd := exec.Command("go", "mod", "tidy") 240 tidyCmd.Stdout = os.Stdout 241 tidyCmd.Stderr = os.Stdout 242 if err := tidyCmd.Run(); err != nil { 243 return fmt.Errorf("go mod tidy failed: %w", err) 244 } 245 return nil 246 } 247 248 // Errors returns any errors that were returned by Load, either from the call itself or any of the loaded packages. 249 func (p *Packages) Errors() PkgErrors { 250 var res []error //nolint:prealloc 251 res = append(res, p.loadErrors...) 252 for _, pkg := range p.packages { 253 for _, err := range pkg.Errors { 254 res = append(res, err) 255 } 256 } 257 return res 258 } 259 260 func (p *Packages) Count() int { 261 return len(p.packages) 262 } 263 264 type PkgErrors []error 265 266 func (p PkgErrors) Error() string { 267 var b bytes.Buffer 268 b.WriteString("packages.Load: ") 269 for _, e := range p { 270 b.WriteString(e.Error() + "\n") 271 } 272 return b.String() 273 }