github.com/stackb/rules_proto@v0.0.0-20240221195024-5428336c51f1/pkg/rule/rules_go/go_library.go (about) 1 package rules_go 2 3 import ( 4 "fmt" 5 "log" 6 "path" 7 "strings" 8 9 "github.com/bazelbuild/bazel-gazelle/config" 10 "github.com/bazelbuild/bazel-gazelle/label" 11 "github.com/bazelbuild/bazel-gazelle/resolve" 12 "github.com/bazelbuild/bazel-gazelle/rule" 13 14 "github.com/stackb/rules_proto/pkg/protoc" 15 ) 16 17 const ( 18 ProtoGoLibraryRuleName = "proto_go_library" 19 goLibraryRuleSuffix = "_go_proto" 20 ) 21 22 func init() { 23 protoc.Rules().MustRegisterRule("stackb:rules_proto:"+ProtoGoLibraryRuleName, 24 &goLibrary{ 25 kindName: ProtoGoLibraryRuleName, 26 protoLibrariesByRule: make(map[label.Label][]protoc.ProtoLibrary), 27 }) 28 } 29 30 // goLibrary implements LanguageRule for the '{proto|grpc}_go_library' rule from 31 // @rules_proto. 32 type goLibrary struct { 33 kindName string 34 protoLibrariesByRule map[label.Label][]protoc.ProtoLibrary 35 } 36 37 // Name implements part of the LanguageRule interface. 38 func (s *goLibrary) Name() string { 39 return s.kindName 40 } 41 42 // KindInfo implements part of the LanguageRule interface. 43 func (s *goLibrary) KindInfo() rule.KindInfo { 44 return rule.KindInfo{ 45 MatchAttrs: []string{"importpath"}, 46 NonEmptyAttrs: map[string]bool{ 47 "deps": true, 48 "embed": true, 49 "srcs": true, 50 }, 51 MergeableAttrs: map[string]bool{ 52 "embed": true, 53 "srcs": true, 54 }, 55 ResolveAttrs: map[string]bool{"deps": true}, 56 } 57 } 58 59 // LoadInfo implements part of the LanguageRule interface. 60 func (s *goLibrary) LoadInfo() rule.LoadInfo { 61 return rule.LoadInfo{ 62 Name: fmt.Sprintf("@build_stack_rules_proto//rules/go:%s.bzl", s.kindName), 63 Symbols: []string{s.kindName}, 64 } 65 } 66 67 // ProvideRule implements part of the LanguageRule interface. 68 func (s *goLibrary) ProvideRule(cfg *protoc.LanguageRuleConfig, pc *protoc.ProtocConfiguration) protoc.RuleProvider { 69 // collect the outputs and the deps. Search all the PluginConfigurations. 70 // If the produced .go files, include them and add their deps. 71 outputs := make([]string, 0) 72 pluginDeps := make([]string, 0) 73 74 for _, pluginConfig := range pc.Plugins { 75 for _, out := range pluginConfig.Outputs { 76 if path.Ext(out) == ".go" { 77 outputs = append(outputs, out) 78 pluginDeps = append(pluginDeps, pluginConfig.Config.GetDeps()...) 79 } 80 } 81 } 82 83 if len(outputs) == 0 { 84 return nil 85 } 86 87 for i, output := range outputs { 88 outputs[i] = path.Join(pc.Rel, path.Base(output)) 89 } 90 91 rule := &goLibraryRule{ 92 kindName: s.kindName, 93 ruleNameSuffix: goLibraryRuleSuffix, 94 outputs: protoc.DeduplicateAndSort(outputs), 95 deps: protoc.DeduplicateAndSort(pluginDeps), 96 ruleConfig: cfg, 97 pc: pc, 98 protoLibrariesByRule: s.protoLibrariesByRule, 99 } 100 rule.id = label.New("", pc.Rel, rule.Name()) 101 return rule 102 } 103 104 // goLibraryRule implements RuleProvider for 'go_library'-derived rules. 105 type goLibraryRule struct { 106 id label.Label 107 kindName string 108 ruleNameSuffix string 109 outputs []string 110 deps []string 111 pc *protoc.ProtocConfiguration 112 ruleConfig *protoc.LanguageRuleConfig 113 protoLibrariesByRule map[label.Label][]protoc.ProtoLibrary 114 } 115 116 // Kind implements part of the ruleProvider interface. 117 func (s *goLibraryRule) Kind() string { 118 return s.kindName 119 } 120 121 // Name implements part of the ruleProvider interface. 122 func (s *goLibraryRule) Name() string { 123 return s.pc.Library.BaseName() + s.ruleNameSuffix 124 } 125 126 // Srcs computes the srcs list for the rule. 127 func (s *goLibraryRule) Srcs() []string { 128 srcs := make([]string, 0) 129 for _, output := range s.outputs { 130 srcs = append(srcs, protoc.StripRel(s.pc.Rel, output)) 131 } 132 return srcs 133 } 134 135 // deps returns all known "configured" dependencies: 136 // 1. Those given by the plugin implementations that contributed outputs (their 'deps' directive). 137 // 2. Those given by 'deps' directive on the rule config. 138 // 3. Those given by resolving "rewrite" specs against the proto file imports. 139 func (s *goLibraryRule) configDeps() []string { 140 deps := s.deps 141 deps = append(deps, s.ruleConfig.GetDeps()...) 142 resolvedDeps := protoc.ResolveLibraryRewrites(s.ruleConfig.GetRewrites(), s.pc.Library) 143 deps = append(deps, resolvedDeps...) 144 return deps 145 } 146 147 // Visibility provides visibility labels. 148 func (s *goLibraryRule) Visibility() []string { 149 return s.ruleConfig.GetVisibility() 150 } 151 152 func (s *goLibraryRule) goPrefix() string { 153 res := protoc.GlobalResolver().Resolve("gazelle", "directive", "prefix") 154 if len(res) == 0 { 155 return "" 156 } 157 return res[0].Label.Pkg 158 } 159 160 // importPath computes the import path. 161 func (s *goLibraryRule) importPath() string { 162 // Try 'M' options first 163 if imp := s.getPluginImportMappingOption(); imp != "" { 164 return imp 165 } 166 167 // Next try the 'go_package' option in an imported file 168 for _, file := range s.pc.Library.Files() { 169 if value, _ := protoc.GetNamedOption(file.Options(), "go_package"); value != "" { 170 if strings.LastIndexByte(value, '/') == -1 { 171 // return langgo.InferImportPath(c, rel) 172 continue // TODO: do more research here on if this is the correct approach 173 } 174 if i := strings.LastIndexByte(value, ';'); i != -1 { 175 return value[:i] 176 } 177 return value 178 } 179 } 180 181 // fallback to 'gazelle:prefix + rel' 182 prefix := s.goPrefix() 183 if prefix == "" { 184 return "" 185 } 186 187 pkg := s.pc.Rel 188 name := s.pc.Library.BaseName() 189 190 return path.Join(prefix, pkg, name) 191 } 192 193 // Rule implements part of the ruleProvider interface. 194 func (s *goLibraryRule) Rule(otherGen ...*rule.Rule) *rule.Rule { 195 importpath := s.importPath() 196 srcs := s.Srcs() 197 deps := s.configDeps() 198 visibility := s.Visibility() 199 imports := s.pc.Library.Imports() 200 201 // Check if an existing proto_go_library rule has already been generated 202 // under this importpath. If so, we need to merge into it rather than 203 // create a new rule. 204 for _, other := range otherGen { 205 if other.Kind() == ProtoGoLibraryRuleName && other.AttrString("importpath") == importpath { 206 otherLabel := label.New("", s.pc.Rel, other.Name()) 207 otherSrcs := other.AttrStrings("srcs") 208 otherDeps := other.AttrStrings("deps") 209 otherVis := other.AttrStrings("visibility") 210 otherImports := other.PrivateAttr(config.GazelleImportsKey).([]string) 211 212 other.SetAttr("srcs", protoc.DeduplicateAndSort(append(otherSrcs, srcs...))) 213 other.SetAttr("deps", protoc.DeduplicateAndSort(append(otherDeps, deps...))) 214 other.SetAttr("visibility", protoc.DeduplicateAndSort(append(otherVis, visibility...))) 215 other.SetPrivateAttr(config.GazelleImportsKey, protoc.DeduplicateAndSort(append(otherImports, imports...))) 216 217 s.protoLibrariesByRule[otherLabel] = append(s.protoLibrariesByRule[otherLabel], s.pc.Library) 218 219 return other 220 } 221 } 222 223 newRule := rule.NewRule(s.Kind(), s.Name()) 224 newRule.SetAttr("srcs", srcs) 225 newRule.SetPrivateAttr(config.GazelleImportsKey, imports) 226 s.protoLibrariesByRule[s.id] = []protoc.ProtoLibrary{s.pc.Library} 227 228 if importpath != "" { 229 newRule.SetAttr("importpath", importpath) 230 } 231 if len(deps) > 0 { 232 newRule.SetAttr("deps", deps) 233 } 234 if len(visibility) > 0 { 235 newRule.SetAttr("visibility", visibility) 236 } 237 return newRule 238 } 239 240 func printProtoLibraryNames(libs []protoc.ProtoLibrary) string { 241 names := make([]string, len(libs)) 242 for i, lib := range libs { 243 names[i] = lib.BaseName() 244 } 245 return strings.Join(names, ",") 246 } 247 248 // Imports implements part of the RuleProvider interface. 249 func (s *goLibraryRule) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec { 250 // for the cross-resolver such that go can cross-resolve this library 251 from := label.New("", f.Pkg, r.Name()) 252 253 // log.Println("provide for cross-resolver", r.AttrString("importpath"), from) 254 protoc.GlobalResolver().Provide("go", "go", r.AttrString("importpath"), from) 255 256 libs, ok := s.protoLibrariesByRule[s.id] 257 if !ok { 258 return nil 259 } 260 261 return protoc.ProtoLibraryImportSpecsForKind(r.Kind(), libs...) 262 } 263 264 // Resolve implements part of the RuleProvider interface. 265 func (s *goLibraryRule) Resolve(c *config.Config, ix *resolve.RuleIndex, r *rule.Rule, imports []string, from label.Label) { 266 protoc.ResolveDepsAttr("deps", true)(c, ix, r, imports, from) 267 268 // need to make one more pass to possibly move deps into embeds. There may 269 // be dependencies *IN OTHER PACKAGES* that have the same importpath; in 270 // that case we need to embed, not depend. 271 all := r.AttrStrings("deps") 272 273 deps := make([]string, 0) 274 embeds := make([]string, 0) 275 importpath := r.AttrString("importpath") 276 277 for _, dep := range all { 278 dl, err := label.Parse(dep) 279 if err != nil { 280 log.Printf("resolve deps failed for for rule %s %s: label parse %q error: %v", r.Kind(), r.Name(), dep, err) 281 deps = append(deps, dep) 282 continue 283 } 284 285 // If this is a relative label, make it absolute 286 if dl.Repo == "" && dl.Pkg == "" { 287 dl = label.Label{Pkg: s.pc.Rel, Name: dl.Name} 288 } 289 290 // retrieve the rule for this label 291 if dr := protoc.GlobalRuleIndex().Get(dl); dr != nil { 292 depImportpath := dr.AttrString("importpath") 293 // if it has the same importpath, need to embed this 294 if depImportpath == importpath { 295 embeds = append(embeds, dep) 296 continue 297 } 298 } 299 300 deps = append(deps, dep) 301 } 302 303 if len(deps) > 0 { 304 r.SetAttr("deps", protoc.DeduplicateAndSort(deps)) 305 } 306 if len(embeds) > 0 { 307 r.SetAttr("embed", protoc.DeduplicateAndSort(embeds)) 308 } 309 } 310 311 func (s *goLibraryRule) getPluginImportMappingOption() string { 312 // first, iterate all the plugins and gather options that look like 313 // protoc-gen-go "importmapping" (M) options (e.g 314 // Mfoo.proto=github.com/example/foo). 315 mappings := make(map[string]string) 316 317 tryParseMapping := func(opt string) { 318 if !strings.HasPrefix(opt, "M") { 319 return 320 } 321 parts := strings.SplitN(opt[1:], "=", 2) 322 if len(parts) != 2 { 323 return 324 } 325 mappings[parts[0]] = parts[1] 326 } 327 328 // search all plugins 329 for _, plugin := range s.pc.Plugins { 330 for _, opt := range plugin.Options { 331 tryParseMapping(opt) 332 } 333 } 334 // and all rule options 335 for _, opt := range s.ruleConfig.GetOptions() { 336 tryParseMapping(opt) 337 } 338 339 // now that we've gathered all possible options; search all library files 340 // (e.g. foo.proto) and see if we can find a match. 341 for _, file := range s.pc.Library.Files() { 342 filename := path.Join(file.Dir, file.Basename) 343 mapping := mappings[filename] 344 if mapping != "" { 345 return mapping 346 } 347 } 348 349 return "" 350 }