Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ builds:
goarch: arm64
mod_timestamp: "{{ .CommitTimestamp }}"
hooks:
post: # sign the windows binaries
post:
Comment thread
zeroshade marked this conversation as resolved.
Outdated
- if: '{{ and (eq .Os "windows") (isEnvSet "SM_API_KEY") }}'
cmd: >-
smctl sign --keypair-alias "{{ .Env.SM_KEYPAIR_ALIAS }}" --input "{{ .Path }}" --config-file "{{ .Env.PKCS11_CONFIG }}"
Expand Down
113 changes: 113 additions & 0 deletions cmd/dbc/latest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// 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 main

import (
"errors"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

"charm.land/lipgloss/v2"
"github.com/Masterminds/semver/v3"
"github.com/cli/safeexec"
"github.com/columnar-tech/dbc"
"github.com/columnar-tech/dbc/internal"
)

func isUnderHomebrew(bin string) bool {
brewExe, err := safeexec.LookPath("brew")
if err != nil {
return false
}

brewPrefixBytes, err := exec.Command(brewExe, "--prefix").Output()
if err != nil {
return false
}

brewBinPrefix := filepath.Join(strings.TrimSpace(
string(brewPrefixBytes)), "bin") + string(filepath.Separator)
return strings.HasPrefix(bin, brewBinPrefix)
}

func isPkgMgrInstall() bool {
exe, err := os.Executable()
if err != nil {
return false
}

if isUnderHomebrew(exe) {
return true
}

exe, err = filepath.EvalSymlinks(exe)
if err != nil {
return false
}

switch filepath.Dir(exe) {
case "/usr/local/bin":
// pip installs here on linux
return true
case "/usr/bin":
// likely a system package manager, but could be other things too
return true
default:
// likely a local user install via script
// or via msi on windows etc.
// this is the case where we want to notify about updates
}

if runtime.GOOS == "windows" && strings.HasSuffix(filepath.Dir(exe), "\\Scripts") {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a check for the MSI installer too? It would cover manual MSI installation and also winget.

// likely a pip install on windows
return true
}

if strings.Contains(exe, "conda") || strings.Contains(exe, "venv") {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hrm, this is harder than I thought. When I install dbc into a conda env on macOS, I get a path of:

/opt/homebrew/Caskroom/miniforge/base/envs/test/bin/dbc

In a root Docker container: /root/miniforge3/bin/dbc
In a non-root Docker container: /home/test/miniforge3/envs/test/bin/dbc

Should we expand this check to include 'envs' so it at least covers a non-root miniforge3 setup? What are the conda-based setup where "conda" is part of the path? I wonder if we shouldn't change how the venv/conda check works to be more like brew. We could check env vars for being in a conda prefix or venv and compare the exe path with it.

On Windows I get C:\Users\Bryce\miniforge3\Scripts\dbc.exe so the windows check looks fine.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's because you're using miniforge/mamba instead of conda 😄 if you use conda directly you get conda in the path

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured, but I'll point out that miniforge is one of the three main ways conda recommends installing conda.

// likely a conda or virtual environment install
return true
}

return false
}

func notifyLatest() {
if isPkgMgrInstall() {
// skip notifying if installed via package manager,
// since they likely have their own update mechanism
return
}

configDir, err := internal.GetDbcConfigPath()
if err != nil {
return
}

// skip notifying if $dbc_config_home/.no-update exists
_, err = os.Stat(filepath.Join(configDir, ".no-update"))
if errors.Is(err, os.ErrNotExist) {
latestVer, err := dbc.GetLatestDbcVersion()
if dbc.Version != "(devel)" && err == nil {
if semver.MustParse(dbc.Version).LessThan(latestVer) {
lipgloss.Printf(descStyle.Render("Update available: A new version of dbc is available. You're running %s and v%s is available. Please upgrade.\nChangelog: %s. Docs: %s"),
Comment thread
zeroshade marked this conversation as resolved.
Outdated
dbc.Version, latestVer, "https://github.com/columnar-tech/dbc/releases/tag/"+latestVer.String(), "https://docs.columnar.tech/dbc/getting_started/installation/")
Comment thread
zeroshade marked this conversation as resolved.
Outdated
lipgloss.Println()
}
}
}
}
4 changes: 4 additions & 0 deletions cmd/dbc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ func main() {
}
}

if !args.Quiet {
notifyLatest()
}

if m, err = prog.Run(); err != nil {
fmt.Fprintln(os.Stderr, "Error running program:", err)
os.Exit(1)
Expand Down
17 changes: 17 additions & 0 deletions docs/getting_started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,23 @@ dbc can generate shell completions for a number of common shells.

You can use the `dbc completion` subcommand to print extended instructions for your shell, including how to enable your shell's completion mechanism. For example, to print setup instructions for Bash, run `dbc completion bash --help`.

## Updating

{{ since_version('v0.2.0') }}

If you installed dbc using the [Standalone Installer](#standalone-installer) or manually through [GitHub Releases](#github-releases), dbc will automatically notify you if your version is out of date and will provide instructions for updating.

For other installation methods, dbc won't automatically notify of updates and you should upgrade dbc in whatever way is standard for your installation method.

!!! note
To silence update notifications, create an empty file called `.no-update` in the dbc configuration directory for your operating system:

- Linux: `~/.config/columnar/dbc/.no-update`
- macOS: `~/Library/Application Support/Columnar/dbc/.no-update`
- Windows: `%AppData%/Columnar/dbc/.no-update`

If you use a custom `$XDG_CONFIG_HOME`, use `$XDG_CONFIG_HOME/columnar/dbc/.no-update`.

## Uninstallation

To remove dbc from your system, run the uninstall command corresponding to your installation method.
Expand Down
14 changes: 14 additions & 0 deletions drivers.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,17 @@ func SignedByColumnar(lib, sig io.Reader) error {

return result.SignatureError()
}

func GetLatestDbcVersion() (*semver.Version, error) {
resp, err := makereq("https://dbc.columnar.tech")
Comment thread
ianmcook marked this conversation as resolved.
if err != nil {
return nil, fmt.Errorf("failed to fetch latest dbc version: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusNoContent {
return nil, fmt.Errorf("failed to fetch latest dbc version: %s", resp.Status)
}

return semver.NewVersion(resp.Header.Get("x-dbc-latest"))
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ require (
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/cli/safeexec v1.0.1 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEf
github.com/cli/oauth v1.2.1 h1:9+vketSVuBCbEIpx4XPHHDlTX2R9MbLnM79sfA2Ac+4=
github.com/cli/oauth v1.2.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
Expand Down
19 changes: 14 additions & 5 deletions internal/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ func GetUserConfigPath() (string, error) {
return finalDir, nil
}

// Directory for dbc credentials. This dir is distinct from GetUserConfigPath
// except for on macOS where it's the same
func GetCredentialPath() (string, error) {
func GetDbcConfigPath() (string, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this change the location of .no-update to $HOME/.local/share on Linux? I was thinking we wanted it in XDG_CONFIG_HOME since it's not a credential and the user would probably want the file to roam.

It it helps to rename these funcs, I think paths.go just needs two functions:

  1. Return the XDG_CONFIG_HOME equivalent (for config)
  2. Return the XDG_DATA_HOME equivalent (for credentials)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, this was my mistake. I undid this change and just have notifyLatest call internal.GetUserConfigPath() which uses os.UserConfigDir() which will use XDG_CONFIG_HOME etc.

dir := os.Getenv("XDG_DATA_HOME")
if dir == "" {
switch runtime.GOOS {
Expand All @@ -58,7 +56,7 @@ func GetCredentialPath() (string, error) {
if err != nil {
return "", fmt.Errorf("failed to get user config directory: %w", err)
}
return filepath.Join(userdir, "credentials", "credentials.toml"), nil
return userdir, nil
default: // unix
home, err := os.UserHomeDir()
if err != nil {
Expand All @@ -70,5 +68,16 @@ func GetCredentialPath() (string, error) {
return "", errors.New("path in $XDG_DATA_HOME is relative")
}

return filepath.Join(dir, "dbc", "credentials", "credentials.toml"), nil
return filepath.Join(dir, "dbc"), nil
}

// Directory for dbc credentials. This dir is distinct from GetUserConfigPath
// except for on macOS where it's the same
func GetCredentialPath() (string, error) {
dir, err := GetDbcConfigPath()
if err != nil {
return "", fmt.Errorf("failed to get dbc config path: %w", err)
}

return filepath.Join(dir, "credentials", "credentials.toml"), nil
}
Loading