github.com/stackb/rules_proto@v0.0.0-20240221195024-5428336c51f1/pkg/protoc/depsresolver.go (about) 1 package protoc 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "path" 8 "sort" 9 "strings" 10 11 "github.com/bazelbuild/bazel-gazelle/config" 12 "github.com/bazelbuild/bazel-gazelle/label" 13 "github.com/bazelbuild/bazel-gazelle/resolve" 14 "github.com/bazelbuild/bazel-gazelle/rule" 15 ) 16 17 const ( 18 ResolverLangName = "protobuf" 19 // ResolverImpLangPrivateKey stores the implementation language override. 20 ResolverImpLangPrivateKey = "_protobuf_imp_lang" 21 UnresolvedDepsPrivateKey = "_unresolved_deps" 22 ) 23 24 var ( 25 errSkipImport = errors.New("self import") 26 errNotFound = errors.New("rule not found") 27 ErrNoLabel = errors.New("no label") 28 ) 29 30 const debug = false 31 32 type DepsResolver func(c *config.Config, ix *resolve.RuleIndex, r *rule.Rule, imports []string, from label.Label) 33 34 // ResolveDepsAttr returns a function that implements the DepsResolver 35 // interface. This function resolves dependencies for a given rule attribute 36 // name (typically "deps"); if there is a non-empty list of resolved 37 // dependencies, the rule attribute will be overrwritten/modified by this 38 // function (excluding duplicates, sorting applied). The "from" argument 39 // represents the rule being resolved (whose state is the *rule.Rule argument). 40 // The "imports" list represents the list of imports that was originally 41 // returned by the GenerateResponse.Imports (typically in via a private attr 42 // GazelleImportsKey), and holds the values of all the import statements (e.g. 43 // "google/protobuf/descriptor.proto") of the ProtoLibrary used to generate the 44 // rule. Special handling is provided for well-known types, which can be 45 // excluded using the `excludeWkt` argument. Actual resolution for an 46 // individual import is delegated to the `resolveAnyKind` function. 47 func ResolveDepsAttr(attrName string, excludeWkt bool) DepsResolver { 48 return func(c *config.Config, ix *resolve.RuleIndex, r *rule.Rule, imports []string, from label.Label) { 49 if debug { 50 log.Printf("%v (%s.%s): resolving %d imports: %v", from, r.Kind(), attrName, len(imports), imports) 51 } 52 53 existing := r.AttrStrings(attrName) 54 r.DelAttr(attrName) 55 56 depSet := make(map[string]bool) 57 for _, d := range existing { 58 depSet[d] = true 59 } 60 61 // unresolvedDeps is a mapping from the import string to the reason it 62 // was unresolved. It is saved under 'UnresolvedDepsPrivateKey' if 63 // there were unresolved deps. The value 'ErrNoLabel' is the most 64 // common case. 65 unresolvedDeps := make(map[string]error) 66 67 for _, imp := range imports { 68 if excludeWkt && strings.HasPrefix(imp, "google/protobuf/") { 69 continue 70 } 71 72 // determine the resolve kind 73 impLang := r.Kind() 74 if overrideImpLang, ok := r.PrivateAttr(ResolverImpLangPrivateKey).(string); ok { 75 impLang = overrideImpLang 76 } 77 78 if debug { 79 log.Println(from, "resolving:", imp, impLang) 80 } 81 l, err := resolveAnyKind(c, ix, ResolverLangName, impLang, imp, from) 82 if err == errSkipImport { 83 if debug { 84 log.Println(from, "skipped (errSkipImport):", imp) 85 } 86 continue 87 } 88 if err != nil { 89 log.Println(from, "ResolveDepsAttr error:", err) 90 unresolvedDeps[imp] = err 91 continue 92 } 93 if l == label.NoLabel { 94 if debug { 95 log.Println(from, "no label", imp) 96 } 97 unresolvedDeps[imp] = ErrNoLabel 98 continue 99 } 100 101 l = l.Rel(from.Repo, from.Pkg) 102 if debug { 103 log.Println(from, "resolved:", imp, "is provided by", l) 104 } 105 depSet[l.String()] = true 106 } 107 108 if len(depSet) > 0 { 109 deps := make([]string, 0, len(depSet)) 110 for dep := range depSet { 111 deps = append(deps, dep) 112 } 113 sort.Strings(deps) 114 r.SetAttr(attrName, deps) 115 if debug { 116 log.Println(from, "resolved deps:", deps) 117 } 118 } 119 120 if len(unresolvedDeps) > 0 { 121 r.SetPrivateAttr(UnresolvedDepsPrivateKey, unresolvedDeps) 122 } 123 } 124 } 125 126 // resolveAnyKind answers the question "what bazel label provides a rule for the 127 // given import?" (having the same rule kind as the given rule argument). The 128 // algorithm first consults the override list (configured either via gazelle 129 // resolve directives, or via a YAML config). If no override is found, the 130 // RuleIndex is consulted, which contains all rules indexed by gazelle in the 131 // generation phase. If no match is found, return label.NoLabel. 132 func resolveAnyKind(c *config.Config, ix *resolve.RuleIndex, lang, impLang, imp string, from label.Label) (label.Label, error) { 133 if l, ok := resolve.FindRuleWithOverride(c, resolve.ImportSpec{Lang: impLang, Imp: imp}, lang); ok { 134 // log.Println(from, "override hit:", l) 135 return l, nil 136 } 137 if l, err := resolveWithIndex(c, ix, lang, impLang, imp, from); err == nil || err == errSkipImport { 138 return l, err 139 } else if err != errNotFound { 140 return label.NoLabel, err 141 } 142 if debug { 143 log.Println(from, "fallback miss:", imp) 144 } 145 return label.NoLabel, nil 146 } 147 148 func resolveWithIndex(c *config.Config, ix *resolve.RuleIndex, lang, impLang, imp string, from label.Label) (label.Label, error) { 149 matches := ix.FindRulesByImportWithConfig(c, resolve.ImportSpec{Lang: impLang, Imp: imp}, lang) 150 if len(matches) == 0 { 151 // log.Println(from, "no matches:", imp) 152 return label.NoLabel, errNotFound 153 } 154 if len(matches) > 1 { 155 return label.NoLabel, fmt.Errorf("multiple rules (%s and %s) may be imported with %q from %s", matches[0].Label, matches[1].Label, imp, from) 156 } 157 if matches[0].IsSelfImport(from) || isSameImport(c, from, matches[0].Label) { 158 // log.Println(from, "self import:", imp) 159 return label.NoLabel, errSkipImport 160 } 161 // log.Println(from, "FindRulesByImportWithConfig first match:", imp, matches[0].Label) 162 return matches[0].Label, nil 163 } 164 165 // isSameImport returns true if the "from" and "to" labels are the same. If the 166 // "to" label is not a canonical label (having a fully-qualified repo name), a 167 // canonical label is constructed for comparison using the config.RepoName. 168 func isSameImport(c *config.Config, from, to label.Label) bool { 169 if from == to { 170 return true 171 } 172 if to.Repo != "" { 173 return false 174 } 175 canonical := label.New(c.RepoName, to.Pkg, to.Name) 176 return from == canonical 177 } 178 179 // StripRel removes the rel prefix from a filename (if has matching prefix) 180 func StripRel(rel string, filename string) string { 181 if !strings.HasPrefix(filename, rel) { 182 return filename 183 } 184 filename = filename[len(rel):] 185 return strings.TrimPrefix(filename, "/") 186 } 187 188 // ProtoLibraryImportSpecsForKind generates an ImportSpec for each file in the 189 // set of given proto_library. 190 func ProtoLibraryImportSpecsForKind(kind string, libs ...ProtoLibrary) []resolve.ImportSpec { 191 specs := make([]resolve.ImportSpec, 0) 192 for _, lib := range libs { 193 specs = append(specs, ProtoFilesImportSpecsForKind(kind, lib.Files())...) 194 } 195 196 return specs 197 } 198 199 // ProtoLibraryImportSpecsForKind generates an ImportSpec for each file in the 200 // set of given proto_library. 201 func ProtoFilesImportSpecsForKind(kind string, files []*File) []resolve.ImportSpec { 202 specs := make([]resolve.ImportSpec, 0) 203 for _, file := range files { 204 imp := path.Join(file.Dir, file.Basename) 205 spec := resolve.ImportSpec{Lang: kind, Imp: imp} 206 specs = append(specs, spec) 207 } 208 return specs 209 }