github.com/please-build/puku@v1.7.3-0.20240516143641-f7d7f4941f57/generate/deps.go (about) 1 package generate 2 3 import ( 4 "fmt" 5 "os" 6 "path/filepath" 7 "strings" 8 9 "github.com/please-build/buildtools/build" 10 11 "github.com/please-build/puku/config" 12 "github.com/please-build/puku/fs" 13 "github.com/please-build/puku/kinds" 14 "github.com/please-build/puku/knownimports" 15 ) 16 17 // resolveImport resolves an import path to a build target. It will return an empty string if the import is for a pkg in 18 // the go sdk. Otherwise, it will return the build target for that dependency, or an error if it can't be resolved. If 19 // the target can be resolved to a module that isn't currently added to this project, it will return the build target, 20 // and record the new module in `u.newModules`. These should later be written to the build graph. 21 func (u *updater) resolveImport(conf *config.Config, i string) (string, error) { 22 if t, ok := u.resolvedImports[i]; ok { 23 return t, nil 24 } 25 26 if t := conf.GetKnownTarget(i); t != "" { 27 return t, nil 28 } 29 30 t, err := u.reallyResolveImport(conf, i) 31 if err == nil { 32 u.resolvedImports[i] = t 33 } 34 return t, err 35 } 36 37 // reallyResolveImport actually does the resolution of an import path to a build target. 38 func (u *updater) reallyResolveImport(conf *config.Config, i string) (string, error) { 39 if knownimports.IsInGoRoot(i) { 40 return "", nil 41 } 42 43 if t := u.installs.Get(i); t != "" { 44 return t, nil 45 } 46 47 thirdPartyDir := conf.GetThirdPartyDir() 48 49 // Check to see if the target exists in the current repo 50 if fs.IsSubdir(u.plzConf.ImportPath(), i) || u.plzConf.ImportPath() == "" { 51 t, err := u.localDep(i) 52 if err != nil { 53 return "", err 54 } 55 56 if t != "" { 57 return t, nil 58 } 59 // The above isSubdir check only checks the import path. Modules can have import paths that contain the 60 // current module, so we should carry on here in case we can resolve this to a third party module 61 } 62 63 t := depTarget(u.modules, i, thirdPartyDir) 64 if t != "" { 65 return t, nil 66 } 67 68 // If we're using go_module, we can't automatically add new modules to the graph so we should give up here. 69 if u.usingGoModule { 70 return "", fmt.Errorf("module not found") 71 } 72 73 log.Infof("Resolving module for %v...", i) 74 75 // Otherwise try and resolve it to a new dep via the module proxy. We assume the module will contain the package. 76 // Please will error out in a reasonable way if it doesn't. 77 // TODO it would be more correct to download the module and check it actually contains the package 78 mod, err := u.proxy.ResolveModuleForPackage(i) 79 if err != nil { 80 return "", err 81 } 82 83 log.Infof("Resolved to %v... done", mod.Module) 84 85 // If the package belongs to this module, we should have found this package when resolving local imports above. We 86 // don't want to resolve this like a third party module, so we should return an error here. 87 if mod.Module == u.plzConf.ImportPath() { 88 return "", fmt.Errorf("can't find import %q", i) 89 } 90 91 u.newModules = append(u.newModules, mod) 92 u.modules = append(u.modules, mod.Module) 93 94 // TODO we can probably shortcut this and assume the target is in the above module 95 t = depTarget(u.modules, i, thirdPartyDir) 96 if t != "" { 97 return t, nil 98 } 99 100 return "", fmt.Errorf("module not found") 101 } 102 103 // isInScope returns true when the given path is in scope of the current run i.e. if we are going to format the BUILD 104 // file there. 105 func (u *updater) isInScope(path string) bool { 106 for _, p := range u.paths { 107 if p == path { 108 return true 109 } 110 } 111 return false 112 } 113 114 // localDep finds a dependency local to this repository, checking the BUILD file for a go_library target. Returns an 115 // empty string when no target is found. 116 func (u *updater) localDep(importPath string) (string, error) { 117 path := strings.Trim(strings.TrimPrefix(importPath, u.plzConf.ImportPath()), "/") 118 // If we're using GOPATH based resolution, we don't have a prefix to base whether a path is package local or not. In 119 // this case, we need to check if the directory exists. If it doesn't it's not a local import. 120 if _, err := os.Lstat(path); os.IsNotExist(err) { 121 return "", nil 122 } 123 file, err := u.graph.LoadFile(path) 124 if err != nil { 125 return "", fmt.Errorf("failed to parse BUILD files in %v: %v", path, err) 126 } 127 128 conf, err := config.ReadConfig(path) 129 if err != nil { 130 return "", err 131 } 132 133 var libTargets []*build.Rule 134 for _, rule := range file.Rules("") { 135 kind := conf.GetKind(rule.Kind()) 136 if kind == nil { 137 continue 138 } 139 140 if kind.Type == kinds.Lib { 141 libTargets = append(libTargets, rule) 142 } 143 } 144 145 // If we can't find the lib target, and the target package is in scope for us to potentially generate it, check if 146 // we are going to generate it. 147 if len(libTargets) != 0 { 148 return BuildTarget(libTargets[0].Name(), path, ""), nil 149 } 150 151 if !u.isInScope(importPath) { 152 return "", fmt.Errorf("resolved %v to a local package, but no library target was found and it's not in scope to generate the target", importPath) 153 } 154 155 files, err := ImportDir(path) 156 if err != nil { 157 if os.IsNotExist(err) { 158 return "", nil 159 } 160 return "", fmt.Errorf("failed to import %v: %v", path, err) 161 } 162 163 // If there are any non-test sources, then we will generate a go_library here later on. Return that target name. 164 for _, f := range files { 165 if !f.IsTest() { 166 return BuildTarget(filepath.Base(importPath), path, ""), nil 167 } 168 } 169 return "", nil 170 } 171 172 func depTarget(modules []string, importPath, thirdPartyFolder string) string { 173 module := moduleForPackage(modules, importPath) 174 if module == "" { 175 // If we can't find this import, we can return nothing and the build rule will fail at build time reporting a 176 // sensible error. It may also be an import from the go SDK which is fine. 177 return "" 178 } 179 180 packageName := strings.TrimPrefix(strings.TrimPrefix(importPath, module), "/") 181 return SubrepoTarget(module, thirdPartyFolder, packageName) 182 } 183 184 func moduleForPackage(modules []string, importPath string) string { 185 module := "" 186 for _, mod := range modules { 187 ok := fs.IsSubdir(mod, importPath) 188 if ok && len(mod) > len(module) { 189 module = mod 190 } 191 } 192 return module 193 } 194 195 func SubrepoTarget(module, thirdPartyFolder, packageName string) string { 196 subrepoName := SubrepoName(module, thirdPartyFolder) 197 198 name := filepath.Base(packageName) 199 if packageName == "" { 200 name = filepath.Base(module) 201 } 202 203 return BuildTarget(name, packageName, subrepoName) 204 } 205 206 func SubrepoName(module, thirdPartyFolder string) string { 207 return filepath.Join(thirdPartyFolder, strings.ReplaceAll(module, "/", "_")) 208 } 209 210 func BuildTarget(name, pkgDir, subrepo string) string { 211 bs := new(strings.Builder) 212 if subrepo != "" { 213 bs.WriteString("///") 214 bs.WriteString(subrepo) 215 } 216 217 if pkgDir != "" || subrepo != "" { 218 bs.WriteString("//") 219 } 220 221 if pkgDir == "." { 222 pkgDir = "" 223 } 224 225 if pkgDir != "" { 226 bs.WriteString(pkgDir) 227 if filepath.Base(pkgDir) != name { 228 bs.WriteString(":") 229 bs.WriteString(name) 230 } 231 } else { 232 bs.WriteString(":") 233 bs.WriteString(name) 234 } 235 return bs.String() 236 }