Last active
June 5, 2025 20:10
-
-
Save albertocavalcante/ab1386e77ed8edaa8bd78fbb09a67448 to your computer and use it in GitHub Desktop.
Bzlmod Resolver
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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