From 6950a75fa33cdcf4a9839d65b88dc734a29f091c Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 01:19:19 +0000 Subject: [PATCH] chore(ax): resolve docker and linuxkit runtime commands --- pkg/build/builders/docker.go | 36 ++++++++++++++-------- pkg/build/builders/docker_test.go | 19 ++++++++++++ pkg/release/publishers/docker.go | 41 ++++++++++++++++++------- pkg/release/publishers/docker_test.go | 16 ++++++++++ pkg/release/publishers/linuxkit.go | 29 +++++++++++++---- pkg/release/publishers/linuxkit_test.go | 16 ++++++++++ 6 files changed, 127 insertions(+), 30 deletions(-) diff --git a/pkg/build/builders/docker.go b/pkg/build/builders/docker.go index 6cff56b..18ce795 100644 --- a/pkg/build/builders/docker.go +++ b/pkg/build/builders/docker.go @@ -40,13 +40,13 @@ func (b *DockerBuilder) Detect(fs io.Medium, dir string) (bool, error) { // Build builds Docker images for the specified targets. // Usage example: call value.Build(...) from integrating code. func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) { - // Validate docker CLI is available - if err := b.validateDockerCli(); err != nil { + dockerCommand, err := b.resolveDockerCli() + if err != nil { return nil, err } // Ensure buildx is available - if err := b.ensureBuildx(ctx); err != nil { + if err := b.ensureBuildx(ctx, dockerCommand); err != nil { return nil, err } @@ -162,7 +162,7 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets [] core.Print(nil, " Platforms: %s", core.Join(", ", platforms...)) core.Print(nil, " Tags: %s", core.Join(", ", imageRefs...)) - if err := ax.ExecDir(ctx, cfg.ProjectDir, "docker", args...); err != nil { + if err := ax.ExecDir(ctx, cfg.ProjectDir, dockerCommand, args...); err != nil { return nil, coreerr.E("DockerBuilder.Build", "buildx build failed", err) } @@ -179,25 +179,35 @@ func (b *DockerBuilder) Build(ctx context.Context, cfg *build.Config, targets [] return artifacts, nil } -// validateDockerCli checks if the docker CLI is available. -func (b *DockerBuilder) validateDockerCli() error { - if _, err := ax.LookPath("docker"); err != nil { - return coreerr.E("DockerBuilder.validateDockerCli", "docker CLI not found. Install it from https://docs.docker.com/get-docker/", err) +// resolveDockerCli returns the executable path for the docker CLI. +func (b *DockerBuilder) resolveDockerCli(paths ...string) (string, error) { + if len(paths) == 0 { + paths = []string{ + "/usr/local/bin/docker", + "/opt/homebrew/bin/docker", + "/Applications/Docker.app/Contents/Resources/bin/docker", + } } - return nil + + command, err := ax.ResolveCommand("docker", paths...) + if err != nil { + return "", coreerr.E("DockerBuilder.resolveDockerCli", "docker CLI not found. Install it from https://docs.docker.com/get-docker/", err) + } + + return command, nil } // ensureBuildx ensures docker buildx is available and has a builder. -func (b *DockerBuilder) ensureBuildx(ctx context.Context) error { +func (b *DockerBuilder) ensureBuildx(ctx context.Context, dockerCommand string) error { // Check if buildx is available - if err := ax.Exec(ctx, "docker", "buildx", "version"); err != nil { + if err := ax.Exec(ctx, dockerCommand, "buildx", "version"); err != nil { return coreerr.E("DockerBuilder.ensureBuildx", "buildx is not available. Install it from https://docs.docker.com/buildx/working-with-buildx/", err) } // Check if we have a builder, create one if not - if err := ax.Exec(ctx, "docker", "buildx", "inspect", "--bootstrap"); err != nil { + if err := ax.Exec(ctx, dockerCommand, "buildx", "inspect", "--bootstrap"); err != nil { // Try to create a builder - if err := ax.Exec(ctx, "docker", "buildx", "create", "--use", "--bootstrap"); err != nil { + if err := ax.Exec(ctx, dockerCommand, "buildx", "create", "--use", "--bootstrap"); err != nil { return coreerr.E("DockerBuilder.ensureBuildx", "failed to create buildx builder", err) } } diff --git a/pkg/build/builders/docker_test.go b/pkg/build/builders/docker_test.go index 81f8b15..b740179 100644 --- a/pkg/build/builders/docker_test.go +++ b/pkg/build/builders/docker_test.go @@ -81,3 +81,22 @@ func TestDocker_DockerBuilderInterface_Good(t *testing.T) { var _ build.Builder = (*DockerBuilder)(nil) var _ build.Builder = NewDockerBuilder() } + +func TestDocker_DockerBuilderResolveDockerCli_Good(t *testing.T) { + builder := NewDockerBuilder() + fallbackDir := t.TempDir() + fallbackPath := ax.Join(fallbackDir, "docker") + require.NoError(t, ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755)) + + command, err := builder.resolveDockerCli(fallbackPath) + require.NoError(t, err) + assert.Equal(t, fallbackPath, command) +} + +func TestDocker_DockerBuilderResolveDockerCli_Bad(t *testing.T) { + builder := NewDockerBuilder() + + _, err := builder.resolveDockerCli(ax.Join(t.TempDir(), "missing-docker")) + require.Error(t, err) + assert.Contains(t, err.Error(), "docker CLI not found") +} diff --git a/pkg/release/publishers/docker.go b/pkg/release/publishers/docker.go index 12b78fa..3ec9593 100644 --- a/pkg/release/publishers/docker.go +++ b/pkg/release/publishers/docker.go @@ -54,7 +54,8 @@ func (p *DockerPublisher) Publish(ctx context.Context, release *Release, pubCfg } // Validate docker CLI is available after local config checks. - if err := validateDockerCli(); err != nil { + dockerCommand, err := resolveDockerCli() + if err != nil { return err } @@ -62,7 +63,7 @@ func (p *DockerPublisher) Publish(ctx context.Context, release *Release, pubCfg return p.dryRunPublish(release, dockerCfg) } - return p.executePublish(ctx, release, dockerCfg) + return p.executePublish(ctx, release, dockerCfg, dockerCommand) } // parseConfig extracts Docker-specific configuration. @@ -164,9 +165,9 @@ func (p *DockerPublisher) dryRunPublish(release *Release, cfg DockerConfig) erro } // executePublish builds and pushes Docker images. -func (p *DockerPublisher) executePublish(ctx context.Context, release *Release, cfg DockerConfig) error { +func (p *DockerPublisher) executePublish(ctx context.Context, release *Release, cfg DockerConfig, dockerCommand string) error { // Ensure buildx is available and builder is set up - if err := p.ensureBuildx(ctx); err != nil { + if err := p.ensureBuildx(ctx, dockerCommand); err != nil { return err } @@ -177,7 +178,7 @@ func (p *DockerPublisher) executePublish(ctx context.Context, release *Release, args := p.buildBuildxArgs(cfg, tags, release.Version) publisherPrint("Building and pushing Docker image: %s", cfg.Image) - if err := publisherRun(ctx, release.ProjectDir, nil, "docker", args...); err != nil { + if err := publisherRun(ctx, release.ProjectDir, nil, dockerCommand, args...); err != nil { return coreerr.E("docker.Publish", "buildx build failed", err) } @@ -245,16 +246,16 @@ func (p *DockerPublisher) buildBuildxArgs(cfg DockerConfig, tags []string, versi } // ensureBuildx ensures docker buildx is available and has a builder. -func (p *DockerPublisher) ensureBuildx(ctx context.Context) error { +func (p *DockerPublisher) ensureBuildx(ctx context.Context, dockerCommand string) error { // Check if buildx is available - if err := ax.Exec(ctx, "docker", "buildx", "version"); err != nil { + if err := ax.Exec(ctx, dockerCommand, "buildx", "version"); err != nil { return coreerr.E("docker.ensureBuildx", "buildx is not available. Install it from https://docs.docker.com/buildx/working-with-buildx/", nil) } // Check if we have a builder, create one if not - if err := ax.Exec(ctx, "docker", "buildx", "inspect", "--bootstrap"); err != nil { + if err := ax.Exec(ctx, dockerCommand, "buildx", "inspect", "--bootstrap"); err != nil { // Try to create a builder - if err := publisherRun(ctx, "", nil, "docker", "buildx", "create", "--use", "--bootstrap"); err != nil { + if err := publisherRun(ctx, "", nil, dockerCommand, "buildx", "create", "--use", "--bootstrap"); err != nil { return coreerr.E("docker.ensureBuildx", "failed to create buildx builder", err) } } @@ -262,10 +263,28 @@ func (p *DockerPublisher) ensureBuildx(ctx context.Context) error { return nil } +// resolveDockerCli returns the executable path for the docker CLI. +func resolveDockerCli(paths ...string) (string, error) { + if len(paths) == 0 { + paths = []string{ + "/usr/local/bin/docker", + "/opt/homebrew/bin/docker", + "/Applications/Docker.app/Contents/Resources/bin/docker", + } + } + + command, err := ax.ResolveCommand("docker", paths...) + if err != nil { + return "", coreerr.E("docker.resolveDockerCli", "docker CLI not found. Install it from https://docs.docker.com/get-docker/", err) + } + + return command, nil +} + // validateDockerCli checks if the docker CLI is available. func validateDockerCli() error { - if _, err := ax.LookPath("docker"); err != nil { - return coreerr.E("docker.validateDockerCli", "docker CLI not found. Install it from https://docs.docker.com/get-docker/", nil) + if _, err := resolveDockerCli(); err != nil { + return coreerr.E("docker.validateDockerCli", "docker CLI not found. Install it from https://docs.docker.com/get-docker/", err) } return nil } diff --git a/pkg/release/publishers/docker_test.go b/pkg/release/publishers/docker_test.go index 1d5f971..b469ebe 100644 --- a/pkg/release/publishers/docker_test.go +++ b/pkg/release/publishers/docker_test.go @@ -649,6 +649,22 @@ func TestDocker_ValidateDockerCli_Good(t *testing.T) { }) } +func TestDocker_ResolveDockerCli_Good(t *testing.T) { + fallbackDir := t.TempDir() + fallbackPath := ax.Join(fallbackDir, "docker") + require.NoError(t, ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755)) + + command, err := resolveDockerCli(fallbackPath) + require.NoError(t, err) + assert.Equal(t, fallbackPath, command) +} + +func TestDocker_ResolveDockerCli_Bad(t *testing.T) { + _, err := resolveDockerCli(ax.Join(t.TempDir(), "missing-docker")) + require.Error(t, err) + assert.Contains(t, err.Error(), "docker CLI not found") +} + func TestDocker_DockerPublisherPublishWithCLI_Good(t *testing.T) { // These tests run only when docker CLI is available if err := validateDockerCli(); err != nil { diff --git a/pkg/release/publishers/linuxkit.go b/pkg/release/publishers/linuxkit.go index add478e..3e59754 100644 --- a/pkg/release/publishers/linuxkit.go +++ b/pkg/release/publishers/linuxkit.go @@ -42,8 +42,8 @@ func (p *LinuxKitPublisher) Name() string { // Publish builds LinuxKit images and uploads them to the GitHub release. // Usage example: call value.Publish(...) from integrating code. func (p *LinuxKitPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error { - // Validate linuxkit CLI is available - if err := validateLinuxKitCli(); err != nil { + linuxkitCommand, err := resolveLinuxKitCli() + if err != nil { return err } @@ -75,7 +75,7 @@ func (p *LinuxKitPublisher) Publish(ctx context.Context, release *Release, pubCf return p.dryRunPublish(release, lkCfg, repo) } - return p.executePublish(ctx, release, lkCfg, repo) + return p.executePublish(ctx, release, lkCfg, repo, linuxkitCommand) } // parseConfig extracts LinuxKit-specific configuration. @@ -172,7 +172,7 @@ func (p *LinuxKitPublisher) dryRunPublish(release *Release, cfg LinuxKitConfig, } // executePublish builds LinuxKit images and uploads them. -func (p *LinuxKitPublisher) executePublish(ctx context.Context, release *Release, cfg LinuxKitConfig, repo string) error { +func (p *LinuxKitPublisher) executePublish(ctx context.Context, release *Release, cfg LinuxKitConfig, repo, linuxkitCommand string) error { outputDir := ax.Join(release.ProjectDir, "dist", "linuxkit") // Create output directory @@ -197,7 +197,7 @@ func (p *LinuxKitPublisher) executePublish(ctx context.Context, release *Release // Build the image args := p.buildLinuxKitArgs(cfg.Config, format, outputName, outputDir, arch) publisherPrint("Building LinuxKit image: %s (%s)", outputName, format) - if err := publisherRun(ctx, release.ProjectDir, nil, "linuxkit", args...); err != nil { + if err := publisherRun(ctx, release.ProjectDir, nil, linuxkitCommand, args...); err != nil { return coreerr.E("linuxkit.Publish", "build failed for "+platform+"/"+format, err) } @@ -292,9 +292,26 @@ func (p *LinuxKitPublisher) getFormatExtension(format string) string { } } +// resolveLinuxKitCli returns the executable path for the linuxkit CLI. +func resolveLinuxKitCli(paths ...string) (string, error) { + if len(paths) == 0 { + paths = []string{ + "/usr/local/bin/linuxkit", + "/opt/homebrew/bin/linuxkit", + } + } + + command, err := ax.ResolveCommand("linuxkit", paths...) + if err != nil { + return "", coreerr.E("linuxkit.resolveLinuxKitCli", "linuxkit CLI not found. Install it from https://github.com/linuxkit/linuxkit", err) + } + + return command, nil +} + // validateLinuxKitCli checks if the linuxkit CLI is available. func validateLinuxKitCli() error { - if _, err := ax.LookPath("linuxkit"); err != nil { + if _, err := resolveLinuxKitCli(); err != nil { return coreerr.E("linuxkit.validateLinuxKitCli", "linuxkit CLI not found. Install it from https://github.com/linuxkit/linuxkit", err) } return nil diff --git a/pkg/release/publishers/linuxkit_test.go b/pkg/release/publishers/linuxkit_test.go index f714c05..b2f8324 100644 --- a/pkg/release/publishers/linuxkit_test.go +++ b/pkg/release/publishers/linuxkit_test.go @@ -266,6 +266,22 @@ func TestLinuxKit_ValidateLinuxKitCli_Good(t *testing.T) { }) } +func TestLinuxKit_ResolveLinuxKitCli_Good(t *testing.T) { + fallbackDir := t.TempDir() + fallbackPath := ax.Join(fallbackDir, "linuxkit") + require.NoError(t, ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755)) + + command, err := resolveLinuxKitCli(fallbackPath) + require.NoError(t, err) + assert.Equal(t, fallbackPath, command) +} + +func TestLinuxKit_ResolveLinuxKitCli_Bad(t *testing.T) { + _, err := resolveLinuxKitCli(ax.Join(t.TempDir(), "missing-linuxkit")) + require.Error(t, err) + assert.Contains(t, err.Error(), "linuxkit CLI not found") +} + func TestLinuxKit_LinuxKitPublisherPublishWithCLI_Good(t *testing.T) { // These tests run only when linuxkit CLI is available if err := validateLinuxKitCli(); err != nil {