Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions client_methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,9 @@ func (c *Client) Download(ctx context.Context, pkg PkgInfo) (io.ReadCloser, erro
}

// Install installs a driver with the given name to the specified configuration.
func (c *Client) Install(ctx context.Context, cfg config.Config, driverName string) (*config.Manifest, error) {
// platform selects the driver package platform tuple; when empty, the host
// platform is used.
func (c *Client) Install(ctx context.Context, cfg config.Config, driverName string, platform config.Platform) (*config.Manifest, error) {
drivers, err := c.Search(ctx, driverName)
// Only fail if the driver wasn't found in any registry; partial registry errors
// are acceptable as long as we can still locate the target driver.
Expand All @@ -267,7 +269,9 @@ func (c *Client) Install(ctx context.Context, cfg config.Config, driverName stri
return nil, fmt.Errorf("driver %q not found", driverName)
}

pkg, err := found.GetPackage(nil, config.PlatformTuple(), false)
targetPlatform := platform.Resolve()

pkg, err := found.GetPackage(nil, targetPlatform, false)
if err != nil {
return nil, fmt.Errorf("failed to get package for driver %s: %w", driverName, err)
}
Expand All @@ -278,7 +282,7 @@ func (c *Client) Install(ctx context.Context, cfg config.Config, driverName stri
}
defer os.RemoveAll(filepath.Dir(f.Name()))

manifest, err := config.InstallDriver(cfg, driverName, f)
manifest, err := config.InstallDriver(cfg, driverName, f, targetPlatform)
if err != nil {
return nil, fmt.Errorf("failed to install driver %s: %w", driverName, err)
}
Expand Down
16 changes: 13 additions & 3 deletions client_methods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,25 @@ func TestClientInstall(t *testing.T) {
}

t.Run("installs driver successfully", func(t *testing.T) {
manifest, err := c.Install(t.Context(), cfg, "test-driver-1")
manifest, err := c.Install(t.Context(), cfg, "test-driver-1", "")
require.NoError(t, err)
require.NotNil(t, manifest)
assert.Equal(t, "test-driver-1", manifest.DriverInfo.ID)
assert.NotNil(t, manifest.DriverInfo.Version)
})

t.Run("installs driver for explicit platform", func(t *testing.T) {
const platform = "linux_amd64"
manifest, err := c.Install(t.Context(), cfg, "test-driver-1", config.Platform(platform))
require.NoError(t, err)
require.NotNil(t, manifest)
sharedPath := manifest.DriverInfo.Driver.Shared.Get(platform)
assert.NotEmpty(t, sharedPath)
assert.FileExists(t, sharedPath)
})

t.Run("returns error for nonexistent driver", func(t *testing.T) {
_, err := c.Install(t.Context(), cfg, "nonexistent-driver")
_, err := c.Install(t.Context(), cfg, "nonexistent-driver", "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "nonexistent-driver")
})
Expand All @@ -144,7 +154,7 @@ func TestClientUninstall(t *testing.T) {
Location: tmpDir,
}

_, err := c.Install(t.Context(), cfg, "test-driver-1")
_, err := c.Install(t.Context(), cfg, "test-driver-1", "")
require.NoError(t, err)

manifestPath := filepath.Join(tmpDir, "test-driver-1.toml")
Expand Down
27 changes: 15 additions & 12 deletions cmd/dbc/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type InstallCmd struct {
// URI url.URL `arg:"-u" placeholder:"URL" help:"Base URL for fetching drivers"`
Driver string `arg:"positional,required" help:"Driver to install, optionally with a version constraint (for example: mysql, mysql=0.1.0, mysql>=1,<2)"`
Level config.ConfigLevel `arg:"-l" help:"Config level to install to (user, system)"`
Platform config.Platform `arg:"--platform" placeholder:"TUPLE" help:"Platform tuple to install for (e.g. linux_amd64). Defaults to the host platform."`
Json bool `arg:"--json" help:"Print output as JSON instead of plaintext"`
JsonStreamProgress bool `arg:"--json-stream-progress" help:"Stream progress events as JSON lines (implies --json)"`
NoVerify bool `arg:"--no-verify" help:"Allow installation of drivers without a signature file"`
Expand All @@ -89,6 +90,7 @@ func (c InstallCmd) GetModelCustom(baseModel baseModel) tea.Model {
}
return progressiveInstallModel{
Driver: c.Driver,
platform: c.Platform.Resolve(),
NoVerify: c.NoVerify,
jsonOutput: c.Json || c.JsonStreamProgress,
jsonStreamProgress: c.JsonStreamProgress,
Expand All @@ -111,12 +113,12 @@ func (c InstallCmd) GetModel() tea.Model {
return c.GetModelCustom(defaultBaseModel())
}

func verifySignature(m config.Manifest, noVerify bool) error {
func verifySignature(m config.Manifest, platform string, noVerify bool) error {
if m.Files.Driver == "" || noVerify {
return nil
}

path := filepath.Dir(m.Driver.Shared.Get(config.PlatformTuple()))
path := filepath.Dir(m.Driver.Shared.Get(platform))

lib, err := os.Open(filepath.Join(path, m.Files.Driver))
if err != nil {
Expand Down Expand Up @@ -215,6 +217,7 @@ type progressiveInstallModel struct {
baseModel

Driver string
platform string
VersionInput *semver.Version
NoVerify bool
jsonOutput bool
Expand Down Expand Up @@ -353,8 +356,8 @@ func (m progressiveInstallModel) FinalOutput() string {
installStatus.Message = m.postInstallMessage
}

if !m.insecureNoChecksum && m.installedDriverInfo.Driver.Shared.Get(config.PlatformTuple()) != "" {
driverPath := m.installedDriverInfo.Driver.Shared.Get(config.PlatformTuple())
if !m.insecureNoChecksum && m.installedDriverInfo.Driver.Shared.Get(m.platform) != "" {
driverPath := m.installedDriverInfo.Driver.Shared.Get(m.platform)
chksum, err := checksum(driverPath)
if err != nil && m.jsonOutput {
return marshalEnvelope("error", jsonschema.ErrorResponse{
Expand Down Expand Up @@ -415,14 +418,14 @@ func (m progressiveInstallModel) searchForDriver(list []dbc.Driver) (tea.Model,
return m, func() tea.Msg {
if vers != nil {
vers.IncludePrerelease = m.Pre
pkg, err := d.GetWithConstraint(vers, config.PlatformTuple())
pkg, err := d.GetWithConstraint(vers, m.platform)
if err != nil {
return err
}
return pkg
}

pkg, err := d.GetPackage(nil, config.PlatformTuple(), m.Pre)
pkg, err := d.GetPackage(nil, m.platform, m.Pre)
if err != nil {
if !m.Pre && !d.HasNonPrerelease() {
for _, cfg := range config.Get() {
Expand All @@ -442,8 +445,8 @@ func (m progressiveInstallModel) startDownloading() (tea.Model, tea.Cmd) {
m.state = stDownloading
if m.isAlreadyInstalled() {
m.state = stDone
if m.jsonOutput && !m.insecureNoChecksum && m.conflictingInfo.Driver.Shared.Get(config.PlatformTuple()) != "" {
driverPath := m.conflictingInfo.Driver.Shared.Get(config.PlatformTuple())
if m.jsonOutput && !m.insecureNoChecksum && m.conflictingInfo.Driver.Shared.Get(m.platform) != "" {
driverPath := m.conflictingInfo.Driver.Shared.Get(m.platform)
return m, func() tea.Msg {
chksum, err := checksum(driverPath)
if err != nil {
Expand All @@ -470,7 +473,7 @@ func (m progressiveInstallModel) startInstalling(downloaded *os.File) (tea.Model
if m.isLocal {
driverName := strings.TrimSuffix(
strings.TrimSuffix(filepath.Base(m.Driver), ".tar.gz"), ".tgz")
parts := strings.Split(driverName, "_"+config.PlatformTuple()+"_")
parts := strings.Split(driverName, "_"+m.platform+"_")
if len(parts) < 2 {
m.Driver = driverName
} else {
Expand All @@ -485,7 +488,7 @@ func (m progressiveInstallModel) startInstalling(downloaded *os.File) (tea.Model
}
}

manifest, err := config.InstallDriver(m.cfg, m.Driver, downloaded)
manifest, err := config.InstallDriver(m.cfg, m.Driver, downloaded, m.platform)
if err != nil {
return err
}
Expand Down Expand Up @@ -557,8 +560,8 @@ func (m progressiveInstallModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m = m.addEvent("extract.complete")
m = m.addEvent("verify.start")
return m, func() tea.Msg {
if err := verifySignature(msg, m.NoVerify); err != nil {
path := filepath.Dir(msg.Driver.Shared.Get(config.PlatformTuple()))
if err := verifySignature(msg, m.platform, m.NoVerify); err != nil {
path := filepath.Dir(msg.Driver.Shared.Get(m.platform))
_ = os.RemoveAll(path)
return err
}
Expand Down
23 changes: 23 additions & 0 deletions cmd/dbc/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,3 +629,26 @@ func (suite *SubcommandTestSuite) TestInstallJSON_AlreadyInstalledChecksumFailur
suite.Require().NoError(json.Unmarshal(errEnv.Payload, &errPayload))
suite.Equal("install_failed", errPayload.Code, "expected install_failed error code")
}

func (suite *SubcommandTestSuite) TestInstallWithPlatform() {
const platform = "linux_amd64"
m := InstallCmd{
Driver: "test-driver-1",
Level: suite.configLevel,
Platform: config.Platform(platform),
}.GetModelCustom(testBaseModel())
out := suite.runCmd(m)

suite.validateOutput(
"\r[✓] searching\r\n[✓] downloading\r\n[✓] installing\r\n[✓] verifying signature\r\n",
"\nInstalled test-driver-1 1.1.0 to "+suite.Dir(), out)

driver := suite.getInstalledDriver("test-driver-1")
sharedPath := driver.Driver.Shared.Get(platform)
suite.NotEmpty(sharedPath, "manifest should record the requested platform tuple")
suite.FileExists(sharedPath)
if platform != config.PlatformTuple() {
suite.Empty(driver.Driver.Shared.Get(config.PlatformTuple()),
"manifest should not use the host platform tuple when --platform is set")
}
}
13 changes: 13 additions & 0 deletions cmd/dbc/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ func TestInstallHelpMentionsVersionConstraints(t *testing.T) {
require.Contains(t, out, `dbc install "mysql=0.1.0"`)
require.Contains(t, out, `dbc install "mysql>=1,<2"`)
require.Contains(t, out, "https://docs.columnar.tech/dbc/guides/installing/#version-constraints")
require.Contains(t, out, "--platform")
}

func TestInstallInvalidPlatformRejectedAtParse(t *testing.T) {
args := &cmds{}
p, err := newParser(args)
require.NoError(t, err)

err = p.Parse([]string{"install", "--platform", "noos_noarch", "mysql"})
require.Error(t, err)
require.NotErrorIs(t, err, arg.ErrHelp)
require.Contains(t, err.Error(), "unknown platform")
require.Contains(t, err.Error(), "valid values are:")
}

func TestSubcommandSuggestions(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/dbc/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ func (s syncModel) installDriver(cfg config.Config, item installItem) tea.Cmd {
manifest.DriverInfo.Source = "dbc"
manifest.DriverInfo.Driver.Shared.Set(config.PlatformTuple(), driverPath)

if err := verifySignature(manifest, s.NoVerify); err != nil {
if err := verifySignature(manifest, config.PlatformTuple(), s.NoVerify); err != nil {
_ = os.RemoveAll(finalDir)
prog.Send(fmt.Errorf("failed to verify signature: %w", err))
return
Expand Down
4 changes: 2 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func getEnvConfigDir() string {
return strings.Join(envConfigLoc, string(filepath.ListSeparator))
}

func InstallDriver(cfg Config, shortName string, downloaded *os.File) (Manifest, error) {
func InstallDriver(cfg Config, shortName string, downloaded *os.File, platform string) (Manifest, error) {
var (
loc string
err error
Expand All @@ -247,7 +247,7 @@ func InstallDriver(cfg Config, shortName string, downloaded *os.File) (Manifest,

manifest.DriverInfo.ID = shortName
manifest.DriverInfo.Source = "dbc"
manifest.DriverInfo.Driver.Shared.Set(PlatformTuple(), driverPath)
manifest.DriverInfo.Driver.Shared.Set(platform, driverPath)

return manifest, nil
}
Expand Down
28 changes: 26 additions & 2 deletions config/config_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func TestInstallDriver(t *testing.T) {
f, err := os.Open(filepath.Join("..", "cmd", "dbc", "testdata", "test-driver-1.tar.gz"))
require.NoError(t, err)

manifest, err := config.InstallDriver(cfg, "test-driver-1", f)
manifest, err := config.InstallDriver(cfg, "test-driver-1", f, config.PlatformTuple())
require.NoError(t, err)

assert.Equal(t, "test-driver-1", manifest.DriverInfo.ID)
Expand All @@ -168,6 +168,30 @@ func TestInstallDriver(t *testing.T) {
assert.FileExists(t, sharedPath)
})

t.Run("records explicit platform in manifest", func(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("ADBC_DRIVER_PATH", tmpDir)

cfg := config.Config{
Level: config.ConfigEnv,
Location: tmpDir,
}

f, err := os.Open(filepath.Join("..", "cmd", "dbc", "testdata", "test-driver-1.tar.gz"))
require.NoError(t, err)

const platform = "linux_amd64"
manifest, err := config.InstallDriver(cfg, "test-driver-1", f, platform)
require.NoError(t, err)

sharedPath := manifest.DriverInfo.Driver.Shared.Get(platform)
assert.NotEmpty(t, sharedPath)
assert.FileExists(t, sharedPath)
if platform != config.PlatformTuple() {
assert.Empty(t, manifest.DriverInfo.Driver.Shared.Get(config.PlatformTuple()))
}
})

t.Run("invalid_tarball", func(t *testing.T) {
tmpDir := t.TempDir()
cfg := config.Config{
Expand All @@ -180,7 +204,7 @@ func TestInstallDriver(t *testing.T) {
_, _ = f.Write([]byte("not a tarball"))
_, _ = f.Seek(0, io.SeekStart)

_, err = config.InstallDriver(cfg, "bad-driver", f)
_, err = config.InstallDriver(cfg, "bad-driver", f, config.PlatformTuple())
assert.Error(t, err)
})
}
Expand Down
69 changes: 69 additions & 0 deletions config/platform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2026 Columnar Technologies Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package config

import (
"fmt"
"slices"
"strings"
)

// Platform is an optional install target platform tuple (e.g. "linux_amd64").
// When unset, callers should use PlatformTuple() for the host platform.
type Platform string

// Valid platform tuples as in https://dbc-cdn.columnar.tech/index.yaml
// Please keep up-to-date with the registry index!
var validPlatformTuples = []string{
"linux_amd64",
"linux_arm64",
"macos_amd64",
"macos_arm64",
"windows_amd64",
"windows_arm64",
}

func ValidPlatformTuples() []string {
return slices.Clone(validPlatformTuples)
}

func IsValidPlatformTuple(tuple string) bool {
return slices.Contains(validPlatformTuples, tuple)
}

func (p Platform) String() string {
return string(p)
}

// Resolve returns the explicit platform tuple, or the host platform when unset.
func (p Platform) Resolve() string {
if p == "" {
return PlatformTuple()
}
return string(p)
}

func (p *Platform) UnmarshalText(b []byte) error {
s := strings.TrimSpace(string(b))
if s == "" {
*p = ""
return nil
}
if !IsValidPlatformTuple(s) {
return fmt.Errorf("unknown platform %q, valid values are: %s", s, strings.Join(validPlatformTuples, ", "))
}
*p = Platform(s)
return nil
}
Loading