Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
de1fe6c
feat: add context-aware file locking
eitsupi Sep 29, 2026
e506d3d
fix: clarify Wasm lock scope
eitsupi Sep 29, 2026
6de846b
feat: add atomic file replacement
eitsupi Sep 29, 2026
5805c01
fix: serialize driver uninstall operations
eitsupi Sep 29, 2026
9f97cca
fix: canonicalize driver lock paths
eitsupi Sep 29, 2026
c2e1e2b
fix: harden package archive extraction
eitsupi Sep 29, 2026
683f33f
fix: bound package archive extraction
eitsupi Sep 29, 2026
606cf70
fix: unify driver lock identities
eitsupi Sep 29, 2026
d4403a0
fix: support archive staging on Windows
eitsupi Sep 29, 2026
b312c29
feat: add durable driver registration
eitsupi Sep 29, 2026
f873595
feat: add package install transaction
eitsupi Sep 29, 2026
8f3dae9
feat: record package ownership receipts
eitsupi Sep 29, 2026
a4e7d14
feat: use transactional package installation
eitsupi Sep 29, 2026
9f461c5
fix: protect referenced package generations
eitsupi Sep 29, 2026
a391540
fix: centralize managed package cleanup
eitsupi Sep 29, 2026
969a1f5
feat: make sync installs transactional
eitsupi Sep 29, 2026
ec839a9
fix: clean up legacy package replacements
eitsupi Sep 29, 2026
3c90592
fix: disambiguate package generation identities
eitsupi Sep 29, 2026
4a8bd9f
fix: avoid reporting retained sync drivers as removed
eitsupi Sep 29, 2026
0fe7777
test: preserve direct install fast path
eitsupi Sep 29, 2026
d99c2d4
docs: clarify legacy install helper semantics
eitsupi Sep 29, 2026
6e85317
test: align install checks with config levels
eitsupi Sep 29, 2026
dc255ad
test: account for Windows path semantics
eitsupi Sep 29, 2026
4640a4a
fix: preserve host paths in Wasm installs
eitsupi Sep 29, 2026
fca7d7e
fix: match relative path semantics in Wasm
eitsupi Sep 29, 2026
772a576
test: handle registry-backed sync assertions
eitsupi Sep 29, 2026
8599278
feat: propagate install transaction context
eitsupi Sep 29, 2026
96b989b
fix: close install archive for nil context
eitsupi Sep 29, 2026
32b34df
fix: describe shadowed install conflicts accurately
eitsupi Sep 29, 2026
7594ea5
fix: remove package registration before cleanup
eitsupi Sep 29, 2026
33eaf2a
fix: restore live signature verification phase
eitsupi Sep 29, 2026
26e233b
test: consolidate package cleanup coverage
eitsupi Sep 29, 2026
34b9b98
test: assert exact transaction payload bytes
eitsupi Sep 30, 2026
403b480
fix: serialize Wasm lock directory aliases
eitsupi Sep 30, 2026
bafec7c
test: handle unavailable Windows symlinks in Wasm
eitsupi Sep 30, 2026
12240c9
fix: clarify install conflicts and cleanup diagnostics
eitsupi Sep 30, 2026
187e337
fix: share primary configuration location parsing
eitsupi Sep 30, 2026
bd13c50
fix: confine cleanup and preserve shared payloads
eitsupi Sep 30, 2026
a5b57f6
fix: protect cleanup with filesystem object identity
eitsupi Sep 30, 2026
0ed60fc
fix: align lock identity and Wasm cleanup expectations
eitsupi Sep 30, 2026
0bcedcd
fix: handle shared lock roots and Windows cleanup checks
eitsupi Sep 30, 2026
9a5b67c
fix: retain Unix lock files and document deferred work
eitsupi Sep 30, 2026
e2037f9
test: align empty cleanup expectations on Windows
eitsupi Sep 30, 2026
c8834b9
test: isolate Windows package fixtures
eitsupi Sep 30, 2026
7e8bed2
fix: simplify legacy package ownership cleanup
eitsupi Oct 1, 2026
604b125
feat!: require owned driver payloads in dbc packages
eitsupi Oct 1, 2026
5de9997
feat!: preserve external libraries when uninstalling registrations
eitsupi Oct 1, 2026
55052da
docs: clarify package installation and uninstall ownership
eitsupi Oct 1, 2026
3d358e7
refactor: simplify package receipts for owned driver payloads
eitsupi Oct 1, 2026
5515e96
test: cover package ownership through Wasm bridges
eitsupi Oct 1, 2026
e86e982
test: fix legacy uninstall fixtures across config levels
eitsupi Oct 1, 2026
bf73c78
docs: clarify managed package storage and external registrations
eitsupi Oct 1, 2026
a58f7aa
fix: keep initial package receipt and fingerprint versions at one
eitsupi Oct 1, 2026
1c91954
Merge upstream/main into feat/install-foundation
eitsupi Oct 24, 2026
3c1b90a
fix(config): honor umask for published package generations
eitsupi Oct 24, 2026
8dde8eb
docs: describe hidden package storage and lock files
eitsupi Oct 24, 2026
9a1adf2
refactor: remove unused install and cleanup helpers
eitsupi Oct 24, 2026
6920cdf
refactor: share platform-neutral install helpers and trim comments
eitsupi Oct 24, 2026
e83ead8
refactor(config): group small platform-specific install files
eitsupi Oct 24, 2026
5c2beae
test(config): drop redundant install and cleanup tests
eitsupi Oct 24, 2026
0bf129d
test(config): restore umask even when the umask test fails
eitsupi Oct 24, 2026
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
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ Then in another shell, run `dlv connect` and debug with dlv as you normally woul
```console
$ dlv connect 127.0.0.1:2345
Type 'help' for list of commands.
(dlv) b install.go:58
(dlv) b main.verifySignatureInStaging
(dlv) c
> [Breakpoint 1] main.verifySignature() /Users/user/src/columnar-tech/dbc/cmd/dbc/install.go:58 (hits goroutine(99):1 total:1) (PC: 0x105201f88)
> [Breakpoint 1] main.verifySignatureInStaging() /Users/user/src/columnar-tech/dbc/cmd/dbc/install.go (hits goroutine(99):1 total:1) (PC: 0x105201f88)
```

When you're done, exiting the client should cause the server to exit automatically.
Expand Down
10 changes: 4 additions & 6 deletions client_methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,19 +278,17 @@ 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.InstallPackage(ctx, cfg, driverName, f, config.InstallPackageOptions{})
if err != nil {
return nil, fmt.Errorf("failed to install driver %s: %w", driverName, err)
}

if err := config.CreateManifest(cfg, manifest.DriverInfo); err != nil {
return nil, fmt.Errorf("failed to create manifest for driver %s: %w", driverName, err)
}

return &manifest, nil
}

// Uninstall uninstalls a driver with the given name from the specified configuration.
// Uninstall removes a driver's runtime registration from the specified
// configuration. For external registrations, the shared library remains in
// place because dbc does not own it.
func (c *Client) Uninstall(cfg config.Config, driverName string) error {
di, err := config.GetDriver(cfg, driverName)
if err != nil {
Expand Down
21 changes: 21 additions & 0 deletions client_methods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"strings"
"testing"

"github.com/Masterminds/semver/v3"
"github.com/columnar-tech/dbc"
"github.com/columnar-tech/dbc/config"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -168,6 +169,26 @@ func TestClientUninstall(t *testing.T) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "not-installed-driver")
})

t.Run("removes external registration and preserves shared library", func(t *testing.T) {
tmpDir := t.TempDir()
cfg := config.Config{Level: config.ConfigEnv, Location: tmpDir}
shared := filepath.Join(tmpDir, "managed-elsewhere", "driver.so")
require.NoError(t, os.MkdirAll(filepath.Dir(shared), 0o700))
require.NoError(t, os.WriteFile(shared, []byte("external library"), 0o600))
info := config.DriverInfo{
ID: "external-driver", FilePath: tmpDir, Name: "External Driver",
Version: semver.MustParse("1.0.0"), Source: "external",
}
info.Driver.Shared.Set(config.PlatformTuple(), shared)
require.NoError(t, config.CreateManifest(cfg, info))

require.NoError(t, c.Uninstall(cfg, info.ID))
assert.NoFileExists(t, filepath.Join(tmpDir, "external-driver.toml"))
data, err := os.ReadFile(shared)
require.NoError(t, err)
assert.Equal(t, "external library", string(data))
})
}

func TestClientDownload(t *testing.T) {
Expand Down
130 changes: 91 additions & 39 deletions cmd/dbc/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package main

import (
"context"
"encoding/json"
"errors"
"fmt"
Expand All @@ -32,6 +33,7 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/columnar-tech/dbc"
"github.com/columnar-tech/dbc/config"
"github.com/columnar-tech/dbc/internal/hostpath"
"github.com/columnar-tech/dbc/internal/jsonschema"
)

Expand Down Expand Up @@ -111,13 +113,14 @@ func (c InstallCmd) GetModel() tea.Model {
return c.GetModelCustom(defaultBaseModel())
}

func verifySignature(m config.Manifest, noVerify bool) error {
if m.Files.Driver == "" || noVerify {
func verifySignatureInStaging(path string, m config.Manifest, noVerify bool) error {
if m.Files.Driver == "" {
return fmt.Errorf("package manifest does not specify Files.driver")
}
if noVerify {
return nil
}

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

lib, err := os.Open(filepath.Join(path, m.Files.Driver))
if err != nil {
return fmt.Errorf("could not open driver file: %w", err)
Expand Down Expand Up @@ -145,10 +148,12 @@ func verifySignature(m config.Manifest, noVerify bool) error {
return nil
}

type writeDriverManifestMsg struct {
DriverInfo config.DriverInfo
type packageInstallResultMsg struct {
Manifest config.Manifest
}

type installVerificationStartedMsg struct{}

type localInstallMsg struct{}

// alreadyInstalledChecksumMsg carries the checksum computed for an already-installed driver.
Expand Down Expand Up @@ -241,6 +246,8 @@ type progressiveInstallModel struct {
alreadyInstalledChecksum string
jsonOut io.Writer
jsonErrorOutput string // JSON error envelope to emit via FinalOutput
verificationStarted bool
verifyStagedPackage func(string, config.Manifest, bool) error
}

type driversWithRegistryError struct {
Expand All @@ -257,8 +264,8 @@ func (m progressiveInstallModel) Init() tea.Cmd {

return tea.Batch(m.spinner.Tick, func() tea.Msg {
installDir := "."
if locs := filepath.SplitList(m.cfg.Location); len(locs) > 0 && locs[0] != "" {
installDir = locs[0]
if root := m.cfg.PrimaryLocation(); root != "" {
installDir = root
}
lockDir := installDir
for {
Expand Down Expand Up @@ -313,7 +320,7 @@ func (m progressiveInstallModel) FinalOutput() string {
Status: "already installed",
Driver: m.conflictingInfo.ID,
Version: m.conflictingInfo.Version.String(),
Location: filepath.SplitList(m.cfg.Location)[0],
Location: m.cfg.PrimaryLocation(),
}
if m.alreadyInstalledChecksum != "" {
payload.Checksum = m.alreadyInstalledChecksum
Expand All @@ -334,7 +341,7 @@ func (m progressiveInstallModel) FinalOutput() string {
return string(jsonOutput)
}
return fmt.Sprintf("\nDriver %s %s already installed at %s",
m.conflictingInfo.ID, m.conflictingInfo.Version, filepath.SplitList(m.cfg.Location)[0])
m.conflictingInfo.ID, m.conflictingInfo.Version, m.cfg.PrimaryLocation())
}

var b strings.Builder
Expand All @@ -343,7 +350,7 @@ func (m progressiveInstallModel) FinalOutput() string {
Status: "installed",
Driver: m.Driver,
Version: m.DriverPackage.Version.String(),
Location: filepath.SplitList(m.cfg.Location)[0],
Location: m.cfg.PrimaryLocation(),
}
if m.hasConflict() {
installStatus.Conflict = fmt.Sprintf("%s (version: %s)", m.conflictingInfo.ID, m.conflictingInfo.Version)
Expand Down Expand Up @@ -383,7 +390,11 @@ func (m progressiveInstallModel) FinalOutput() string {
}

if installStatus.Conflict != "" {
fmt.Fprintf(&b, "\nRemoved conflicting driver: %s", installStatus.Conflict)
label := "Shadowed active driver"
if conflictIsInInstallRoot(m.conflictingInfo, m.cfg.PrimaryLocation(), m.cfg.Level) {
label = "Replaced active driver"
}
fmt.Fprintf(&b, "\n%s: %s", label, installStatus.Conflict)
}

fmt.Fprintf(&b, "\nInstalled %s %s to %s",
Expand All @@ -396,6 +407,45 @@ func (m progressiveInstallModel) FinalOutput() string {
return b.String()
}

// TODO: The install-root path identity check below duplicates logic in config;
// consider reusing it from, or moving it to, the config package.
func conflictIsInInstallRoot(conflict config.DriverInfo, installRoot string, level config.ConfigLevel) bool {
if conflict.FilePath == "" || installRoot == "" {
return false
}

// Registry registrations identify their hive rather than a filesystem root.
// They are replaced when this install targets the same registry level.
switch strings.ToUpper(conflict.FilePath) {
case `HKCU\SOFTWARE\ADBC\DRIVERS`:
return level == config.ConfigUser
case `HKLM\SOFTWARE\ADBC\DRIVERS`:
return level == config.ConfigSystem
}

return sameInstallRoot(conflict.FilePath, installRoot)
}

func sameInstallRoot(a, b string) bool {
if aInfo, err := os.Stat(a); err == nil {
if bInfo, err := os.Stat(b); err == nil && os.SameFile(aInfo, bInfo) {
return true
}
}
canonicalize := func(path string) string {
absolute, err := hostpath.Abs(path)
if err != nil {
absolute = hostpath.Clean(path)
}
if resolved, err := hostpath.EvalSymlinks(absolute); err == nil {
absolute = resolved
}
return hostpath.Clean(absolute)
}

return hostpath.Equal(canonicalize(a), canonicalize(b))
}

func (m progressiveInstallModel) searchForDriver(list []dbc.Driver) (tea.Model, tea.Cmd) {
driverName, vers, err := parseDriverConstraint(m.Driver)
if err != nil {
Expand Down Expand Up @@ -478,18 +528,24 @@ func (m progressiveInstallModel) startInstalling(downloaded *os.File) (tea.Model
}
}

program := prog
verifier := m.verifyStagedPackage
if verifier == nil {
verifier = verifySignatureInStaging
}
return m, func() tea.Msg {
if m.conflictingInfo.ID != "" {
if err := config.UninstallDriver(m.cfg, m.conflictingInfo); err != nil {
return err
}
}

manifest, err := config.InstallDriver(m.cfg, m.Driver, downloaded)
manifest, err := config.InstallPackage(context.Background(), m.cfg, m.Driver, downloaded, config.InstallPackageOptions{
Verifier: func(stagingDir string, stagedManifest config.Manifest) error {
if program != nil {
program.Send(installVerificationStartedMsg{})
}
return verifier(stagingDir, stagedManifest, m.NoVerify)
},
})
if err != nil {
return err
}
return manifest
return packageInstallResultMsg{Manifest: manifest}
}
}

Expand Down Expand Up @@ -547,31 +603,27 @@ func (m progressiveInstallModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m = m.addEvent("download.complete")
m = m.addEvent("extract.start")
return m.startInstalling(msg)
case config.Manifest:
if m.DriverPackage.Version == nil {
m.DriverPackage = manifestToPackageInfo(msg)
case installVerificationStartedMsg:
if m.state >= stDone || m.status != 0 || m.verificationStarted {
return m, nil
}

m.state = stVerifying
m.postInstallMessage = strings.Join(msg.PostInstall.Messages, "\n")
m.verificationStarted = true
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()))
_ = os.RemoveAll(path)
return err
}
return writeDriverManifestMsg{DriverInfo: msg.DriverInfo}
if !m.NoVerify {
m.state = stVerifying
}
case writeDriverManifestMsg:
m.state = stDone
m.installedDriverInfo = msg.DriverInfo
return m, nil
case packageInstallResultMsg:
if m.DriverPackage.Version == nil {
m.DriverPackage = manifestToPackageInfo(msg.Manifest)
}
m.postInstallMessage = strings.Join(msg.Manifest.PostInstall.Messages, "\n")
m.installedDriverInfo = msg.Manifest.DriverInfo
m = m.addEvent("verify.complete")
m = m.addEvent("manifest.create")
return m, tea.Sequence(func() tea.Msg {
return config.CreateManifest(m.cfg, msg.DriverInfo)
}, tea.Quit)
m.state = stDone
return m, tea.Quit
case error:
m.status = 1
m.err = msg
Expand Down
Loading
Loading