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  }