1// Copyright 2023 The Bazel Authors. All rights reserved. 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 15package python 16 17import ( 18 "fmt" 19 "log" 20 "os" 21 "path/filepath" 22 "strings" 23 24 "github.com/bazelbuild/bazel-gazelle/config" 25 "github.com/bazelbuild/bazel-gazelle/label" 26 "github.com/bazelbuild/bazel-gazelle/repo" 27 "github.com/bazelbuild/bazel-gazelle/resolve" 28 "github.com/bazelbuild/bazel-gazelle/rule" 29 bzl "github.com/bazelbuild/buildtools/build" 30 "github.com/emirpasic/gods/sets/treeset" 31 godsutils "github.com/emirpasic/gods/utils" 32 33 "github.com/bazelbuild/rules_python/gazelle/pythonconfig" 34) 35 36const languageName = "py" 37 38const ( 39 // resolvedDepsKey is the attribute key used to pass dependencies that don't 40 // need to be resolved by the dependency resolver in the Resolver step. 41 resolvedDepsKey = "_gazelle_python_resolved_deps" 42) 43 44// Resolver satisfies the resolve.Resolver interface. It resolves dependencies 45// in rules generated by this extension. 46type Resolver struct{} 47 48// Name returns the name of the language. This is the prefix of the kinds of 49// rules generated. E.g. py_library and py_binary. 50func (*Resolver) Name() string { return languageName } 51 52// Imports returns a list of ImportSpecs that can be used to import the rule 53// r. This is used to populate RuleIndex. 54// 55// If nil is returned, the rule will not be indexed. If any non-nil slice is 56// returned, including an empty slice, the rule will be indexed. 57func (py *Resolver) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec { 58 cfgs := c.Exts[languageName].(pythonconfig.Configs) 59 cfg := cfgs[f.Pkg] 60 srcs := r.AttrStrings("srcs") 61 provides := make([]resolve.ImportSpec, 0, len(srcs)+1) 62 for _, src := range srcs { 63 ext := filepath.Ext(src) 64 if ext == ".py" { 65 pythonProjectRoot := cfg.PythonProjectRoot() 66 provide := importSpecFromSrc(pythonProjectRoot, f.Pkg, src) 67 provides = append(provides, provide) 68 } 69 } 70 if len(provides) == 0 { 71 return nil 72 } 73 return provides 74} 75 76// importSpecFromSrc determines the ImportSpec based on the target that contains the src so that 77// the target can be indexed for import statements that match the calculated src relative to the its 78// Python project root. 79func importSpecFromSrc(pythonProjectRoot, bzlPkg, src string) resolve.ImportSpec { 80 pythonPkgDir := filepath.Join(bzlPkg, filepath.Dir(src)) 81 relPythonPkgDir, err := filepath.Rel(pythonProjectRoot, pythonPkgDir) 82 if err != nil { 83 panic(fmt.Errorf("unexpected failure: %v", err)) 84 } 85 if relPythonPkgDir == "." { 86 relPythonPkgDir = "" 87 } 88 pythonPkg := strings.ReplaceAll(relPythonPkgDir, "/", ".") 89 filename := filepath.Base(src) 90 if filename == pyLibraryEntrypointFilename { 91 if pythonPkg != "" { 92 return resolve.ImportSpec{ 93 Lang: languageName, 94 Imp: pythonPkg, 95 } 96 } 97 } 98 moduleName := strings.TrimSuffix(filename, ".py") 99 var imp string 100 if pythonPkg == "" { 101 imp = moduleName 102 } else { 103 imp = fmt.Sprintf("%s.%s", pythonPkg, moduleName) 104 } 105 return resolve.ImportSpec{ 106 Lang: languageName, 107 Imp: imp, 108 } 109} 110 111// Embeds returns a list of labels of rules that the given rule embeds. If 112// a rule is embedded by another importable rule of the same language, only 113// the embedding rule will be indexed. The embedding rule will inherit 114// the imports of the embedded rule. 115func (py *Resolver) Embeds(r *rule.Rule, from label.Label) []label.Label { 116 // TODO(f0rmiga): implement. 117 return make([]label.Label, 0) 118} 119 120// Resolve translates imported libraries for a given rule into Bazel 121// dependencies. Information about imported libraries is returned for each 122// rule generated by language.GenerateRules in 123// language.GenerateResult.Imports. Resolve generates a "deps" attribute (or 124// the appropriate language-specific equivalent) for each import according to 125// language-specific rules and heuristics. 126func (py *Resolver) Resolve( 127 c *config.Config, 128 ix *resolve.RuleIndex, 129 rc *repo.RemoteCache, 130 r *rule.Rule, 131 modulesRaw interface{}, 132 from label.Label, 133) { 134 // TODO(f0rmiga): may need to be defensive here once this Gazelle extension 135 // join with the main Gazelle binary with other rules. It may conflict with 136 // other generators that generate py_* targets. 137 deps := treeset.NewWith(godsutils.StringComparator) 138 if modulesRaw != nil { 139 cfgs := c.Exts[languageName].(pythonconfig.Configs) 140 cfg := cfgs[from.Pkg] 141 pythonProjectRoot := cfg.PythonProjectRoot() 142 modules := modulesRaw.(*treeset.Set) 143 it := modules.Iterator() 144 explainDependency := os.Getenv("EXPLAIN_DEPENDENCY") 145 hasFatalError := false 146 MODULES_LOOP: 147 for it.Next() { 148 mod := it.Value().(module) 149 moduleParts := strings.Split(mod.Name, ".") 150 possibleModules := []string{mod.Name} 151 for len(moduleParts) > 1 { 152 // Iterate back through the possible imports until 153 // a match is found. 154 // For example, "from foo.bar import baz" where bar is a variable, we should try 155 // `foo.bar.baz` first, then `foo.bar`, then `foo`. In the first case, the import could be file `baz.py` 156 // in the directory `foo/bar`. 157 // Or, the import could be variable `bar` in file `foo/bar.py`. 158 // The import could also be from a standard module, e.g. `six.moves`, where 159 // the dependency is actually `six`. 160 moduleParts = moduleParts[:len(moduleParts)-1] 161 possibleModules = append(possibleModules, strings.Join(moduleParts, ".")) 162 } 163 errs := []error{} 164 POSSIBLE_MODULE_LOOP: 165 for _, moduleName := range possibleModules { 166 imp := resolve.ImportSpec{Lang: languageName, Imp: moduleName} 167 if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok { 168 if override.Repo == "" { 169 override.Repo = from.Repo 170 } 171 if !override.Equal(from) { 172 if override.Repo == from.Repo { 173 override.Repo = "" 174 } 175 dep := override.String() 176 deps.Add(dep) 177 if explainDependency == dep { 178 log.Printf("Explaining dependency (%s): "+ 179 "in the target %q, the file %q imports %q at line %d, "+ 180 "which resolves using the \"gazelle:resolve\" directive.\n", 181 explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber) 182 } 183 continue MODULES_LOOP 184 } 185 } else { 186 if dep, ok := cfg.FindThirdPartyDependency(moduleName); ok { 187 deps.Add(dep) 188 if explainDependency == dep { 189 log.Printf("Explaining dependency (%s): "+ 190 "in the target %q, the file %q imports %q at line %d, "+ 191 "which resolves from the third-party module %q from the wheel %q.\n", 192 explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber, mod.Name, dep) 193 } 194 continue MODULES_LOOP 195 } else { 196 matches := ix.FindRulesByImportWithConfig(c, imp, languageName) 197 if len(matches) == 0 { 198 // Check if the imported module is part of the standard library. 199 if isStd, err := isStdModule(module{Name: moduleName}); err != nil { 200 log.Println("Error checking if standard module: ", err) 201 hasFatalError = true 202 continue POSSIBLE_MODULE_LOOP 203 } else if isStd { 204 continue MODULES_LOOP 205 } else if cfg.ValidateImportStatements() { 206 err := fmt.Errorf( 207 "%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+ 208 "\t1. Add it as a dependency in the requirements.txt file.\n"+ 209 "\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+ 210 "\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n", 211 moduleName, mod.LineNumber, mod.Filepath, 212 ) 213 errs = append(errs, err) 214 continue POSSIBLE_MODULE_LOOP 215 } 216 } 217 filteredMatches := make([]resolve.FindResult, 0, len(matches)) 218 for _, match := range matches { 219 if match.IsSelfImport(from) { 220 // Prevent from adding itself as a dependency. 221 continue MODULES_LOOP 222 } 223 filteredMatches = append(filteredMatches, match) 224 } 225 if len(filteredMatches) == 0 { 226 continue POSSIBLE_MODULE_LOOP 227 } 228 if len(filteredMatches) > 1 { 229 sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches)) 230 for _, match := range filteredMatches { 231 if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) { 232 sameRootMatches = append(sameRootMatches, match) 233 } 234 } 235 if len(sameRootMatches) != 1 { 236 err := fmt.Errorf( 237 "multiple targets (%s) may be imported with %q at line %d in %q "+ 238 "- this must be fixed using the \"gazelle:resolve\" directive", 239 targetListFromResults(filteredMatches), moduleName, mod.LineNumber, mod.Filepath) 240 errs = append(errs, err) 241 continue POSSIBLE_MODULE_LOOP 242 } 243 filteredMatches = sameRootMatches 244 } 245 matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg) 246 dep := matchLabel.String() 247 deps.Add(dep) 248 if explainDependency == dep { 249 log.Printf("Explaining dependency (%s): "+ 250 "in the target %q, the file %q imports %q at line %d, "+ 251 "which resolves from the first-party indexed labels.\n", 252 explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber) 253 } 254 continue MODULES_LOOP 255 } 256 } 257 } // End possible modules loop. 258 if len(errs) > 0 { 259 // If, after trying all possible modules, we still haven't found anything, error out. 260 joinedErrs := "" 261 for _, err := range errs { 262 joinedErrs = fmt.Sprintf("%s%s\n", joinedErrs, err) 263 } 264 log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), joinedErrs) 265 hasFatalError = true 266 } 267 } 268 if hasFatalError { 269 os.Exit(1) 270 } 271 } 272 resolvedDeps := r.PrivateAttr(resolvedDepsKey).(*treeset.Set) 273 if !resolvedDeps.Empty() { 274 it := resolvedDeps.Iterator() 275 for it.Next() { 276 deps.Add(it.Value()) 277 } 278 } 279 if !deps.Empty() { 280 r.SetAttr("deps", convertDependencySetToExpr(deps)) 281 } 282} 283 284// targetListFromResults returns a string with the human-readable list of 285// targets contained in the given results. 286func targetListFromResults(results []resolve.FindResult) string { 287 list := make([]string, len(results)) 288 for i, result := range results { 289 list[i] = result.Label.String() 290 } 291 return strings.Join(list, ", ") 292} 293 294// convertDependencySetToExpr converts the given set of dependencies to an 295// expression to be used in the deps attribute. 296func convertDependencySetToExpr(set *treeset.Set) bzl.Expr { 297 deps := make([]bzl.Expr, set.Size()) 298 it := set.Iterator() 299 for it.Next() { 300 dep := it.Value().(string) 301 deps[it.Index()] = &bzl.StringExpr{Value: dep} 302 } 303 return &bzl.ListExpr{List: deps} 304} 305