Skip to content

Instantly share code, notes, and snippets.

@albertocavalcante
Last active June 5, 2025 20:10
Show Gist options
  • Save albertocavalcante/ab1386e77ed8edaa8bd78fbb09a67448 to your computer and use it in GitHub Desktop.
Save albertocavalcante/ab1386e77ed8edaa8bd78fbb09a67448 to your computer and use it in GitHub Desktop.
Bzlmod Resolver
package main
import (
"context"
"fmt"
"os/exec"
"strings"
// These are the Bazelisk packages you'd import
"github.com/bazelbuild/bazelisk/config"
"github.com/bazelbuild/bazelisk/core"
// "github.com/bazelbuild/bazelisk/repositories" // Not directly used by DefaultBazelExecutor methods but by GetBazelInstallation caller or its internals
)
// BazelExecutor defines the interface for interacting with Bazel,
// facilitating mocking and dependency injection for testing.
type BazelExecutor interface {
// GetInstallation resolves Bazel installation details (path, effective startup options,
// and environment) based on the provided Bazelisk configuration.
GetInstallation(ctx context.Context, cfg config.Config) (*core.BazelInstallation, error)
// ExecuteWithInstallation runs a Bazel command using the specified installation details,
// in the given workspace directory, and returns its standard output.
ExecuteWithInstallation(
ctx context.Context,
installation *core.BazelInstallation, // Details from GetInstallation
workspaceDir string,
commandAndSubArgs []string, // e.g., ["mod", "graph", "--output=json"]
) ([]byte, error)
}
// DefaultBazelExecutor is the standard implementation of BazelExecutor,
// using Bazelisk's core functionalities and os/exec for command execution.
type DefaultBazelExecutor struct{}
// NewDefaultBazelExecutor creates a new instance of DefaultBazelExecutor.
func NewDefaultBazelExecutor() *DefaultBazelExecutor {
return &DefaultBazelExecutor{}
}
// GetInstallation uses Bazelisk's core.GetBazelInstallation to resolve
// and potentially download the Bazel executable and determine its execution parameters.
// The 'repos' argument to core.GetBazelInstallation can be nil to allow Bazelisk
// to use its default repository discovery mechanisms (e.g., GCSRepo for releases).
func (dbe *DefaultBazelExecutor) GetInstallation(ctx context.Context, cfg config.Config) (*core.BazelInstallation, error) {
// Passing 'nil' for repositories lets Bazelisk use its default setup.
// If custom repository configurations were needed (e.g., for forks or private GCS buckets),
// a `core.Repositories` struct would be constructed and passed here.
installation, err := core.GetBazelInstallation(nil, cfg)
if err != nil {
return nil, fmt.Errorf("bazelisk core.GetBazelInstallation failed: %w", err)
}
return installation, nil
}
// ExecuteWithInstallation runs the Bazel command.
// It uses installation.BazelPath, installation.BazelStartupOpts, and installation.BazelEnv,
// which are all determined by Bazelisk's GetInstallation based on the input config.Config.
func (dbe *DefaultBazelExecutor) ExecuteWithInstallation(
ctx context.Context,
installation *core.BazelInstallation,
workspaceDir string,
commandAndSubArgs []string,
) ([]byte, error) {
// Prepend Bazel's resolved startup options to the specific command and its arguments.
// installation.BazelStartupOpts already includes what was passed in cfg.StartupOpts
// as processed/merged by Bazelisk.
fullArgsList := append(installation.BazelStartupOpts, commandAndSubArgs...)
cmd := exec.CommandContext(ctx, installation.BazelPath, fullArgsList...)
cmd.Dir = workspaceDir
// installation.BazelEnv is the complete environment Bazelisk determined,
// including OS environment, cfg.Env, and other Bazelisk-derived variables.
cmd.Env = installation.BazelEnv
// For debugging, you might want to log the exact command and environment:
// fmt.Printf("Executing in %s: %s %s\n", workspaceDir, installation.BazelPath, strings.Join(fullArgsList, " "))
// fmt.Printf("With Environment: %v\n", cmd.Env)
output, err := cmd.Output()
if err != nil {
var stderrOutput string
if exitErr, ok := err.(*exec.ExitError); ok {
stderrOutput = strings.TrimSpace(string(exitErr.Stderr))
}
// Constructing a detailed error message
errMsg := fmt.Sprintf("bazel command execution failed. Path: '%s', Args: '%s', Dir: '%s'",
installation.BazelPath, strings.Join(fullArgsList, " "), workspaceDir)
if stderrOutput != "" {
errMsg = fmt.Sprintf("%s. Stderr: %s", errMsg, stderrOutput)
}
return nil, fmt.Errorf("%s: %w", errMsg, err)
}
return output, nil
}
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
// No longer need os/exec here directly, it's in DefaultBazelExecutor
"github.com/bazelbuild/bazelisk/config" // Import Bazelisk's config package
"github.com/bazelbuild/bazelisk/core"
"github.com/bazelbuild/bazelisk/repositories" // Still needed if we were to construct custom Repositories
)
const (
// ModuleBazelFilename is the standard name for Bazel's module file.
ModuleBazelFilename = "MODULE.bazel"
)
// GraphNode represents a node in the dependency graph output by `bazel mod graph --output=json`.
type GraphNode struct {
Key string `json:"key"`
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Dependencies []*GraphNode `json:"dependencies,omitempty"`
IndirectDependencies []string `json:"indirectDependencies,omitempty"`
Cycles []string `json:"cycles,omitempty"`
Unexpanded *bool `json:"unexpanded,omitempty"`
IsRoot bool `json:"root,omitempty"`
}
// ModuleDetail represents extracted, flattened information about a single module.
type ModuleDetail struct {
Key string
Name string
Version string
IsRoot bool
}
// BzlmodResolverConfig holds user-defined configuration for a BzlmodResolver instance.
type BzlmodResolverConfig struct {
WorkspaceDir string
BazelVersion string
ServerJavaBasePath string
HTTPSProxy string
}
// BzlmodResolver provides methods to interact with Bzlmod dependency resolution.
type BzlmodResolver struct {
workspaceDir string
executor BazelExecutor // Injected dependency
installation *core.BazelInstallation // Stored installation details
}
// NewBzlmodResolver creates a new resolver.
// It uses the provided BazelExecutor to get Bazel installation details.
// The ctx is primarily for the GetInstallation call.
func NewBzlmodResolver(
ctx context.Context,
ourConfig BzlmodResolverConfig,
executor BazelExecutor, // Inject the executor
) (*BzlmodResolver, error) {
if ourConfig.WorkspaceDir == "" {
return nil, fmt.Errorf("WorkspaceDir must be specified in BzlmodResolverConfig")
}
if ourConfig.BazelVersion == "" {
return nil, fmt.Errorf("BazelVersion must be specified in BzlmodResolverConfig")
}
// --- Configure Bazelisk ---
bazeliskCfg := config.MakeDefaultConfig()
bazeliskCfg.BazelVersion = ourConfig.BazelVersion
var validatedServerJavaBasePath string
if ourConfig.ServerJavaBasePath != "" {
absJavaBasePath, err := filepath.Abs(ourConfig.ServerJavaBasePath)
if err != nil {
return nil, fmt.Errorf("failed to get absolute path for server_javabase '%s': %w", ourConfig.ServerJavaBasePath, err)
}
// This OS Stat is a side-effect. For pure unit testing of NewBzlmodResolver *without* FS access,
// this check would need to be abstracted (e.g., via an injected validator).
// For practical purposes, testing with existing/non-existing paths covers this.
if _, err := os.Stat(absJavaBasePath); os.IsNotExist(err) {
return nil, fmt.Errorf("server_javabase path '%s' (resolved to '%s') does not exist or is not accessible", ourConfig.ServerJavaBasePath, absJavaBasePath)
}
validatedServerJavaBasePath = absJavaBasePath
bazeliskCfg.StartupOpts = append(bazeliskCfg.StartupOpts, fmt.Sprintf("--server_javabase=%s", validatedServerJavaBasePath))
}
if ourConfig.HTTPSProxy != "" {
if bazeliskCfg.Env == nil {
bazeliskCfg.Env = make(map[string]string)
}
bazeliskCfg.Env["HTTPS_PROXY"] = ourConfig.HTTPSProxy
}
installation, err := executor.GetInstallation(ctx, bazeliskCfg)
if err != nil {
return nil, fmt.Errorf("failed to get Bazel installation via executor: %w. Config used: (Version: %s, JavaBase: '%s', Proxy: '%s')",
err, ourConfig.BazelVersion, validatedServerJavaBasePath, ourConfig.HTTPSProxy)
}
absWorkspaceDir, err := filepath.Abs(ourConfig.WorkspaceDir)
if err != nil {
return nil, fmt.Errorf("failed to get absolute path for workspaceDir '%s': %w", ourConfig.WorkspaceDir, err)
}
return &BzlmodResolver{
workspaceDir: absWorkspaceDir,
executor: executor,
installation: installation,
}, nil
}
// Helper to parse module key into name and version.
func parseModuleKey(key string) (name string, version string) {
parts := strings.SplitN(key, "@", 2)
if len(parts) == 2 { return parts[0], parts[1] }
return key, ""
}
// GetDependencyGraph retrieves the complete dependency graph as a GraphNode.
func (r *BzlmodResolver) GetDependencyGraph(ctx context.Context, options ...GraphOption) (*GraphNode, error) {
args := []string{"mod", "graph", "--output=json"}
for _, opt := range options { args = opt.apply(args) }
output, err := r.executor.ExecuteWithInstallation(ctx, r.installation, r.workspaceDir, args)
if err != nil { return nil, err }
var rootNode GraphNode
if err := json.Unmarshal(output, &rootNode); err != nil {
return nil, fmt.Errorf("failed to parse JSON from 'bazel mod graph': %w. Output: %s", err, string(output))
}
return &rootNode, nil
}
// ExplainModule shows why a module is included in the dependency graph.
func (r *BzlmodResolver) ExplainModule(ctx context.Context, moduleNameOrKey string) (string, error) {
args := []string{"mod", "explain", moduleNameOrKey}
output, err := r.executor.ExecuteWithInstallation(ctx, r.installation, r.workspaceDir, args)
if err != nil { return "", err }
return string(output), nil
}
// GetAllPaths finds all paths between modules.
func (r *BzlmodResolver) GetAllPaths(ctx context.Context, fromModuleKey, toModuleKey string) (string, error) {
args := []string{"mod", "all_paths", toModuleKey}
if fromModuleKey != "" { args = append(args, "--from", fromModuleKey) }
output, err := r.executor.ExecuteWithInstallation(ctx, r.installation, r.workspaceDir, args)
if err != nil { return "", err }
return string(output), nil
}
// --- GraphOption Interface and Implementations (Unchanged from previous version) ---
type GraphOption interface{ apply(args []string) []string }
type includeUnused struct{}
func (includeUnused) apply(args []string) []string { return append(args, "--include_unused") }
func WithIncludeUnused() GraphOption { return includeUnused{} }
type depth struct{ value int }
func (d depth) apply(args []string) []string { return append(args, fmt.Sprintf("--depth=%d", d.value)) }
func WithDepth(d int) GraphOption { return depth{value: d} }
type verbose struct{}
func (verbose) apply(args []string) []string { return append(args, "--verbose") }
func WithVerbose() GraphOption { return verbose{} }
type extensionInfo struct{ mode string }
func (e extensionInfo) apply(args []string) []string { return append(args, fmt.Sprintf("--extension_info=%s", e.mode)) }
func WithExtensionInfo(mode string) GraphOption { return extensionInfo{mode: mode} }
type registryOption struct{ registries []string }
func (r registryOption) apply(args []string) []string {
for _, reg := range r.registries { args = append(args, fmt.Sprintf("--registry=%s", reg)) }
return args
}
func WithRegistry(registries ...string) GraphOption { return registryOption{registries: registries} }
type overrideModuleOption struct{ moduleKeyOrName, overrideValue string }
func (o overrideModuleOption) apply(args []string) []string {
return append(args, fmt.Sprintf("--override_module=%s=%s", o.moduleKeyOrName, o.overrideValue))
}
func WithOverrideModule(moduleKeyOrName, overrideValue string) GraphOption {
return overrideModuleOption{moduleKeyOrName: moduleKeyOrName, overrideValue: overrideValue}
}
// --- Ad-hoc Module Resolution Functions ---
// AdhocGraphConfig provides shared configuration for ad-hoc graph resolution functions.
type AdhocGraphConfig struct {
BazelVersion string
ServerJavaBasePath string
HTTPSProxy string
Options []GraphOption
}
// GetDependencyGraphForModuleFile resolves graph for a specific MODULE.bazel file.
// It now requires a BazelExecutor for testability.
func GetDependencyGraphForModuleFile(ctx context.Context, moduleFilePath string, config AdhocGraphConfig, executor BazelExecutor) (*GraphNode, error) {
if filepath.Base(moduleFilePath) != ModuleBazelFilename {
return nil, fmt.Errorf("moduleFilePath must be a path to a %s file, got: %s", ModuleBazelFilename, moduleFilePath)
}
workspaceDir := filepath.Dir(moduleFilePath)
resolverConfig := BzlmodResolverConfig{
WorkspaceDir: workspaceDir,
BazelVersion: config.BazelVersion,
ServerJavaBasePath: config.ServerJavaBasePath,
HTTPSProxy: config.HTTPSProxy,
}
resolver, err := NewBzlmodResolver(ctx, resolverConfig, executor) // Pass executor
if err != nil {
return nil, fmt.Errorf("failed to create resolver for module file '%s': %w", moduleFilePath, err)
}
return resolver.GetDependencyGraph(ctx, config.Options...)
}
// GetDependencyGraphForModuleContent resolves graph for ad-hoc MODULE.bazel content.
// It now requires a BazelExecutor.
func GetDependencyGraphForModuleContent(ctx context.Context, moduleFileContent string, config AdhocGraphConfig, executor BazelExecutor) (*GraphNode, error) {
tempDir, err := os.MkdirTemp("", "bzlmod-temp-") // Side-effect: FS
if err != nil { return nil, fmt.Errorf("failed to create temp workspace: %w", err) }
defer os.RemoveAll(tempDir) // Side-effect: FS
if err := os.WriteFile(filepath.Join(tempDir, ModuleBazelFilename), []byte(moduleFileContent), 0644); err != nil { // Side-effect: FS
return nil, fmt.Errorf("failed to write temp %s: %w", ModuleBazelFilename, err)
}
resolverConfig := BzlmodResolverConfig{
WorkspaceDir: tempDir,
BazelVersion: config.BazelVersion,
ServerJavaBasePath: config.ServerJavaBasePath,
HTTPSProxy: config.HTTPSProxy,
}
resolver, err := NewBzlmodResolver(ctx, resolverConfig, executor) // Pass executor
if err != nil { return nil, fmt.Errorf("failed to create resolver for temp content: %w", err) }
return resolver.GetDependencyGraph(ctx, config.Options...)
}
// GetDependencyGraphForSingleBazelDep resolves graph for a single bazel_dep entry.
// It now requires a BazelExecutor.
func GetDependencyGraphForSingleBazelDep(ctx context.Context, depName, depVersion string, config AdhocGraphConfig, executor BazelExecutor) (*GraphNode, error) {
safeModuleNamePart := strings.ReplaceAll(strings.ReplaceAll(depName, "-", "_"), ".", "_")
moduleContent := fmt.Sprintf(
`module(name = "temp_mod_for_%s", version = "0.0.0")%sbazel_dep(name = "%s", version = "%s")`,
safeModuleNamePart, "\n", depName, depVersion,
)
return GetDependencyGraphForModuleContent(ctx, moduleContent, config, executor) // Pass executor
}
// --- GraphNode Helper Methods (Unchanged from previous version) ---
// ExtractModuleDetails traverses the graph and returns a flat list of unique modules.
func (node *GraphNode) ExtractModuleDetails() []ModuleDetail {
detailsMap := make(map[string]ModuleDetail)
var traverse func(n *GraphNode, isEntryPoint bool)
traverse = func(n *GraphNode, isEntryPoint bool) {
if n == nil { return }
effectiveIsRoot := isEntryPoint || n.IsRoot
if existingDetail, ok := detailsMap[n.Key]; ok {
if effectiveIsRoot && !existingDetail.IsRoot {
existingDetail.IsRoot = true; detailsMap[n.Key] = existingDetail
}
} else {
parsedName, parsedVersion := parseModuleKey(n.Key)
if n.Name != "" { parsedName = n.Name }
if n.Version != "" { parsedVersion = n.Version }
detailsMap[n.Key] = ModuleDetail{
Key: n.Key, Name: parsedName, Version: parsedVersion, IsRoot: effectiveIsRoot,
}
}
for _, dep := range n.Dependencies { traverse(dep, false) }
}
traverse(node, true) // Initial call, this node is the entry point
resultList := make([]ModuleDetail, 0, len(detailsMap))
for _, detail := range detailsMap { resultList = append(resultList, detail) }
return resultList
}
// FindNodeByKey searches for a node with the given key within this graph.
func (node *GraphNode) FindNodeByKey(key string) *GraphNode {
if node == nil { return nil }
var foundNode *GraphNode
visited := make(map[string]bool)
var search func(n *GraphNode)
search = func(n *GraphNode) {
if n == nil || visited[n.Key] || foundNode != nil { return }
visited[n.Key] = true
if n.Key == key { foundNode = n; return }
for _, dep := range n.Dependencies { search(dep); if foundNode != nil { return } }
}
search(node)
return foundNode
}
// GetTransitiveDependenciesForNode collects all unique dependency keys for this node.
func (node *GraphNode) GetTransitiveDependenciesForNode() []string {
if node == nil { return nil }
allDepsSet := make(map[string]struct{})
visitedInTraversal := make(map[string]bool)
var collect func(currentNode *GraphNode)
collect = func(currentNode *GraphNode) {
if currentNode == nil || visitedInTraversal[currentNode.Key] { return }
visitedInTraversal[currentNode.Key] = true
for _, dep := range currentNode.Dependencies {
if dep == nil { continue }; allDepsSet[dep.Key] = struct{}{}; collect(dep)
}
}
for _, dep := range node.Dependencies {
if dep == nil { continue }; allDepsSet[dep.Key] = struct{}{}; collect(dep)
}
resultList := make([]string, 0, len(allDepsSet))
for k := range allDepsSet { resultList = append(resultList, k) }
return resultList
}
// --- Example Usage ---
func main() {
ctx := context.Background()
defaultBazelVersion := "7.1.1"
myJDKPath := "" // SET THIS to a valid JDK path to test --server_javabase
myProxyURL := "" // SET THIS to a proxy URL (e.g. "http://localhost:8080") to test https_proxy
// Create the default executor. In tests, you would inject a mock executor.
executor := NewDefaultBazelExecutor()
separator := func() { fmt.Println(strings.Repeat("-", 70)) }
// --- 1. Resolver for current directory ---
fmt.Println("## 1. Resolving graph for MODULE.bazel in current directory ##")
currentDir, _ := os.Getwd()
localModulePath := filepath.Join(currentDir, ModuleBazelFilename)
createdDummyLocalModule := false
if _, err := os.Stat(localModulePath); os.IsNotExist(err) {
fmt.Printf("Creating temporary %s in %s for example...\n", ModuleBazelFilename, currentDir)
dummyContent := `module(name = "main_ws_demo", version = "0.0.1"); bazel_dep(name = "bazel_skylib", version = "1.5.0")`
if err := os.WriteFile(localModulePath, []byte(dummyContent), 0644); err != nil {
fmt.Printf("! Failed to write temporary %s: %v.\n", ModuleBazelFilename, err)
} else {
createdDummyLocalModule = true
}
}
if _, err := os.Stat(localModulePath); err == nil {
resolverConfig := BzlmodResolverConfig{
WorkspaceDir: ".", BazelVersion: defaultBazelVersion,
ServerJavaBasePath: myJDKPath, HTTPSProxy: myProxyURL,
}
logConfig := []string{}
if myJDKPath != "" { logConfig = append(logConfig, fmt.Sprintf("ServerJavaBase: %s", myJDKPath)) }
if myProxyURL != "" { logConfig = append(logConfig, fmt.Sprintf("HTTPS_PROXY: %s", myProxyURL)) }
if len(logConfig) > 0 { fmt.Printf("INFO: Resolver configured with -> %s\n", strings.Join(logConfig, ", "))}
resolver, err := NewBzlmodResolver(ctx, resolverConfig, executor)
if err != nil {
fmt.Printf("! Error creating resolver: %v\n", err)
} else {
fmt.Println("Fetching graph for current directory...")
rootGraphNode, err := resolver.GetDependencyGraph(ctx, WithIncludeUnused())
if err != nil {
fmt.Printf("! Error getting dependency graph: %v\n", err)
} else if rootGraphNode != nil {
fmt.Printf(" Graph Root Key: %s, Name: %s, Version: %s\n", rootGraphNode.Key, rootGraphNode.Name, rootGraphNode.Version)
moduleReport := rootGraphNode.ExtractModuleDetails()
fmt.Printf(" Total unique modules in graph: %d\n", len(moduleReport))
// ... (rest of example output)
}
}
}
if createdDummyLocalModule { os.Remove(localModulePath); fmt.Println("Cleaned up temporary MODULE.bazel.") }
separator()
// --- 2. Ad-hoc graph for single bazel_dep ---
fmt.Println("\n## 2. Resolving graph for a single bazel_dep (e.g., rules_java) ##")
adhocConf := AdhocGraphConfig{
BazelVersion: defaultBazelVersion, ServerJavaBasePath: myJDKPath,
HTTPSProxy: myProxyURL, Options: []GraphOption{WithDepth(1)},
}
depName := "rules_java"; depVersion := "7.6.0" // Check for current version if needed
logConfigAdhoc := []string{}
if myJDKPath != "" { logConfigAdhoc = append(logConfigAdhoc, fmt.Sprintf("ServerJavaBase: %s", myJDKPath)) }
if myProxyURL != "" { logConfigAdhoc = append(logConfigAdhoc, fmt.Sprintf("HTTPS_PROXY: %s", myProxyURL)) }
if len(logConfigAdhoc) > 0 { fmt.Printf("INFO: Adhoc configured with -> %s\n", strings.Join(logConfigAdhoc, ", "))}
fmt.Printf("Fetching graph for single bazel_dep: %s@%s\n", depName, depVersion)
// Pass the executor to the ad-hoc function
singleDepGraph, err := GetDependencyGraphForSingleBazelDep(ctx, depName, depVersion, adhocConf, executor)
if err != nil {
fmt.Printf("! Error getting graph for single dep (%s@%s): %v\n", depName, depVersion, err)
} else if singleDepGraph != nil {
fmt.Printf(" Graph Root Key: %s, Name: %s, Version: %s\n", singleDepGraph.Key, singleDepGraph.Name, singleDepGraph.Version)
moduleReport := singleDepGraph.ExtractModuleDetails()
fmt.Printf(" Total unique modules in graph: %d\n", len(moduleReport))
for _, mod := range moduleReport {
fmt.Printf(" - Key: %s, Name: %s, Version: %s, IsRoot: %t\n", mod.Key, mod.Name, mod.Version, mod.IsRoot)
}
}
separator()
// You would similarly pass 'executor' to GetDependencyGraphForModuleFile and GetDependencyGraphForModuleContent
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment