From a9e3af2d787f611538ca4b59908dcdcd7a226393 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 8 Feb 2026 14:47:52 +0000 Subject: [PATCH] Add vendor operation for Go, Cargo, Bundler, rebar3, and pip --- definitions/bundler.yaml | 8 ++++++ definitions/cargo.yaml | 8 ++++++ definitions/gomod.yaml | 8 ++++++ definitions/pip.yaml | 8 ++++++ definitions/rebar3.yaml | 8 ++++++ generic_manager.go | 14 +++++++++ generic_manager_test.go | 57 ++++++++++++++++++++++++++++++++++++ manager.go | 3 ++ translator_test.go | 62 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 176 insertions(+) diff --git a/definitions/bundler.yaml b/definitions/bundler.yaml index dc1e688..5d7c954 100644 --- a/definitions/bundler.yaml +++ b/definitions/bundler.yaml @@ -111,6 +111,13 @@ commands: 0: success 1: error + # bundle cache caches .gem files into vendor/cache + vendor: + base: [cache] + exit_codes: + 0: success + 1: error + capabilities: - install - install_frozen @@ -122,3 +129,4 @@ capabilities: - outdated - json_output - path + - vendor diff --git a/definitions/cargo.yaml b/definitions/cargo.yaml index ca7e9b0..f3aaf90 100644 --- a/definitions/cargo.yaml +++ b/definitions/cargo.yaml @@ -114,6 +114,13 @@ commands: extract_field: manifest_path strip_filename: true + # cargo vendor copies crate sources into vendor/ + vendor: + base: [vendor] + exit_codes: + 0: success + 1: error + capabilities: - install - install_frozen @@ -124,5 +131,6 @@ capabilities: - list - workspace - path + - vendor # No json_output for tree by default # No native outdated diff --git a/definitions/gomod.yaml b/definitions/gomod.yaml index 4ffb2e3..44d9f80 100644 --- a/definitions/gomod.yaml +++ b/definitions/gomod.yaml @@ -110,6 +110,13 @@ commands: type: json field: Dir + # go mod vendor copies all dependencies into vendor/ + vendor: + base: [mod, vendor] + exit_codes: + 0: success + 1: error + capabilities: - install - add @@ -119,4 +126,5 @@ capabilities: - outdated - json_output - path + - vendor # No add_dev - Go doesn't have dev dependencies diff --git a/definitions/pip.yaml b/definitions/pip.yaml index a1f0154..ddcfc46 100644 --- a/definitions/pip.yaml +++ b/definitions/pip.yaml @@ -88,6 +88,13 @@ commands: type: line_prefix prefix: "Location: " + # pip download fetches packages into a directory + vendor: + base: [download, -r, requirements.txt, -d, vendor] + exit_codes: + 0: success + 1: error + capabilities: - install - add @@ -97,3 +104,4 @@ capabilities: - update - json_output - path + - vendor diff --git a/definitions/rebar3.yaml b/definitions/rebar3.yaml index fc8b5bb..8a476dd 100644 --- a/definitions/rebar3.yaml +++ b/definitions/rebar3.yaml @@ -76,8 +76,16 @@ commands: type: template pattern: "_build/default/lib/{package}" + # rebar3 vendor turns dependencies into top-level apps + vendor: + base: [vendor] + exit_codes: + 0: success + 1: error + capabilities: - install - list - update - path + - vendor diff --git a/generic_manager.go b/generic_manager.go index e7eef55..e16875d 100644 --- a/generic_manager.go +++ b/generic_manager.go @@ -151,6 +151,20 @@ func (m *GenericManager) Capabilities() []Capability { return caps } +func (m *GenericManager) Vendor(ctx context.Context) (*Result, error) { + input := CommandInput{ + Args: map[string]string{}, + Flags: map[string]any{}, + } + + cmd, err := m.translator.BuildCommand(m.def.Name, "vendor", input) + if err != nil { + return nil, err + } + + return m.runner.Run(ctx, m.dir, cmd...) +} + func (m *GenericManager) Path(ctx context.Context, pkg string) (*PathResult, error) { input := CommandInput{ Args: map[string]string{ diff --git a/generic_manager_test.go b/generic_manager_test.go index c954b3c..e246eaf 100644 --- a/generic_manager_test.go +++ b/generic_manager_test.go @@ -305,6 +305,63 @@ func TestGenericManager_Path_NoPathCommand(t *testing.T) { } } +func TestGenericManager_Vendor(t *testing.T) { + def := &definitions.Definition{ + Name: "gomod", + Binary: "go", + Commands: map[string]definitions.Command{ + "vendor": { + Base: []string{"mod", "vendor"}, + }, + }, + Capabilities: []string{"vendor"}, + } + + runner := NewMockRunner() + runner.Results = []*Result{{ + ExitCode: 0, + Stdout: "", + }} + + mgr := newTestManager(def, runner) + result, err := mgr.Vendor(context.Background()) + if err != nil { + t.Fatalf("Vendor failed: %v", err) + } + + if result.ExitCode != 0 { + t.Errorf("got exit code %d, want 0", result.ExitCode) + } + + if len(runner.Captured) != 1 { + t.Fatalf("expected 1 command, got %d", len(runner.Captured)) + } + expected := []string{"go", "mod", "vendor"} + if !slicesEqual(runner.Captured[0], expected) { + t.Errorf("got command %v, want %v", runner.Captured[0], expected) + } +} + +func TestGenericManager_Vendor_NoCommand(t *testing.T) { + def := &definitions.Definition{ + Name: "testpkg", + Binary: "testpkg", + Commands: map[string]definitions.Command{ + "install": { + Base: []string{"install"}, + }, + }, + Capabilities: []string{"install"}, + } + + runner := NewMockRunner() + mgr := newTestManager(def, runner) + _, err := mgr.Vendor(context.Background()) + if err == nil { + t.Error("expected error for missing vendor command, got nil") + } +} + func slicesEqual(a, b []string) bool { if len(a) != len(b) { return false diff --git a/manager.go b/manager.go index ae038a1..767c588 100644 --- a/manager.go +++ b/manager.go @@ -16,6 +16,7 @@ type Manager interface { Outdated(ctx context.Context) (*Result, error) Update(ctx context.Context, pkg string) (*Result, error) Path(ctx context.Context, pkg string) (*PathResult, error) + Vendor(ctx context.Context) (*Result, error) Supports(cap Capability) bool Capabilities() []Capability @@ -80,6 +81,7 @@ const ( CapSBOMCycloneDX CapSBOMSPDX CapPath + CapVendor ) var capabilityNames = map[Capability]string{ @@ -99,6 +101,7 @@ var capabilityNames = map[Capability]string{ CapSBOMCycloneDX: "sbom_cyclonedx", CapSBOMSPDX: "sbom_spdx", CapPath: "path", + CapVendor: "vendor", } func (c Capability) String() string { diff --git a/translator_test.go b/translator_test.go index 5f7fc46..91caba5 100644 --- a/translator_test.go +++ b/translator_test.go @@ -3294,3 +3294,65 @@ func TestRebar3Path(t *testing.T) { t.Errorf("got %v, want %v", cmd, expected) } } + +// --- vendor tests --- + +func TestGomodVendor(t *testing.T) { + tr := loadTranslator(t) + cmd, err := tr.BuildCommand("gomod", "vendor", CommandInput{}) + if err != nil { + t.Fatalf("BuildCommand failed: %v", err) + } + expected := []string{"go", "mod", "vendor"} + if !reflect.DeepEqual(cmd, expected) { + t.Errorf("got %v, want %v", cmd, expected) + } +} + +func TestCargoVendor(t *testing.T) { + tr := loadTranslator(t) + cmd, err := tr.BuildCommand("cargo", "vendor", CommandInput{}) + if err != nil { + t.Fatalf("BuildCommand failed: %v", err) + } + expected := []string{"cargo", "vendor"} + if !reflect.DeepEqual(cmd, expected) { + t.Errorf("got %v, want %v", cmd, expected) + } +} + +func TestBundlerVendor(t *testing.T) { + tr := loadTranslator(t) + cmd, err := tr.BuildCommand("bundler", "vendor", CommandInput{}) + if err != nil { + t.Fatalf("BuildCommand failed: %v", err) + } + expected := []string{"bundle", "cache"} + if !reflect.DeepEqual(cmd, expected) { + t.Errorf("got %v, want %v", cmd, expected) + } +} + +func TestRebar3Vendor(t *testing.T) { + tr := loadTranslator(t) + cmd, err := tr.BuildCommand("rebar3", "vendor", CommandInput{}) + if err != nil { + t.Fatalf("BuildCommand failed: %v", err) + } + expected := []string{"rebar3", "vendor"} + if !reflect.DeepEqual(cmd, expected) { + t.Errorf("got %v, want %v", cmd, expected) + } +} + +func TestPipVendor(t *testing.T) { + tr := loadTranslator(t) + cmd, err := tr.BuildCommand("pip", "vendor", CommandInput{}) + if err != nil { + t.Fatalf("BuildCommand failed: %v", err) + } + expected := []string{"pip", "download", "-r", "requirements.txt", "-d", "vendor"} + if !reflect.DeepEqual(cmd, expected) { + t.Errorf("got %v, want %v", cmd, expected) + } +}