diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 69747a62..e2d27ea0 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -52,7 +52,9 @@ builds: goarch: arm64 mod_timestamp: "{{ .CommitTimestamp }}" hooks: - post: # sign the windows binaries + post: # sign the windows binaries in-place with smctl, which is required + # for the Microsoft Store. We don't produce detached signatures for + # windows binaries since that's not really a thing on windows and smctl doesn't support it. - 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 }}" diff --git a/cmd/dbc/latest.go b/cmd/dbc/latest.go new file mode 100644 index 00000000..018b3898 --- /dev/null +++ b/cmd/dbc/latest.go @@ -0,0 +1,122 @@ +// 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" + "strings" + "time" + + "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 + } + + if isManaged(exe) { + return true + } + + if strings.Contains(exe, "conda") || strings.Contains(exe, "venv") || strings.Contains(exe, "miniforge") { + // likely a conda or virtual environment install + return true + } + + return false +} + +func writeLastUpdateCheck(configDir string) { + // file doesn't exist, create it with current timestamp and skip update check + _ = os.MkdirAll(configDir, 0o700) + _ = os.WriteFile(filepath.Join(configDir, ".last-update-check"), []byte(time.Now().Format(time.DateOnly)), 0o600) +} + +func notifyLatest() { + configDir, err := internal.GetUserConfigPath() + if err != nil { + return + } + + // skip notifying if $dbc_config_home/.no-update exists + _, err = os.Stat(filepath.Join(configDir, ".no-update")) + if err == nil { + return // file exists, skip update check + } + + lastUpdate, err := os.ReadFile(filepath.Join(configDir, ".last-update-check")) + if errors.Is(err, os.ErrNotExist) { + writeLastUpdateCheck(configDir) + } else if err == nil { + lastCheckTime, err := time.Parse(time.DateOnly, string(lastUpdate)) + if err != nil { + // if the file is corrupted, reset it + _ = os.WriteFile(filepath.Join(configDir, ".last-update-check"), []byte(time.Now().Format(time.DateOnly)), 0o600) + } else if time.Since(lastCheckTime) > 24*time.Hour { + writeLastUpdateCheck(configDir) + } else { + return // last check was within 24 hours, skip update check + } + } + + if isPkgMgrInstall() { + // skip notifying if installed via package manager, + // since they likely have their own update mechanism + return + } + + latestVer, err := dbc.GetLatestDbcVersion() + if dbc.Version != "(devel)" && err == nil { + if semver.MustParse(dbc.Version).LessThan(latestVer) { + lipgloss.Fprintf(os.Stderr, descStyle.Render("Update available: A new version of dbc is available. You're running v%s and v%s is available. Please upgrade.\nChangelog: %s. Docs: %s"), + dbc.Version, latestVer, "https://github.com/columnar-tech/dbc/releases/tag/v"+latestVer.String(), "https://docs.columnar.tech/dbc/getting_started/installation/") + lipgloss.Fprintln(os.Stderr) + } + } +} diff --git a/cmd/dbc/latest_darwin.go b/cmd/dbc/latest_darwin.go new file mode 100644 index 00000000..48de62d7 --- /dev/null +++ b/cmd/dbc/latest_darwin.go @@ -0,0 +1,26 @@ +// 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 "path/filepath" + +func isManaged(exe string) bool { + if filepath.Dir(exe) == "/usr/local/bin" { + // system-wide pip install + return true + } + + return false +} diff --git a/cmd/dbc/latest_linux.go b/cmd/dbc/latest_linux.go new file mode 100644 index 00000000..9875ac9b --- /dev/null +++ b/cmd/dbc/latest_linux.go @@ -0,0 +1,45 @@ +// 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 ( + "os/exec" + "path/filepath" +) + +func isManaged(exe string) bool { + // check if we're a deb install + dpkgExe, err := exec.LookPath("dpkg") + if err == nil { + if err = exec.Command(dpkgExe, "-S", exe).Run(); err == nil { + return true + } + } + + // check if we're an rpm install + rpmExe, err := exec.LookPath("rpm") + if err == nil { + if err = exec.Command(rpmExe, "-qf", exe).Run(); err == nil { + return true + } + } + + if filepath.Dir(exe) == "/usr/local/bin" { + // pip installs here on linux + return true + } + + return false +} diff --git a/cmd/dbc/latest_windows.go b/cmd/dbc/latest_windows.go new file mode 100644 index 00000000..6119b558 --- /dev/null +++ b/cmd/dbc/latest_windows.go @@ -0,0 +1,44 @@ +// 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 ( + "path/filepath" + "strings" + + "golang.org/x/sys/windows/registry" +) + +func isManaged(exe string) bool { + const packedUpgradeCode = `F0742CB3EE450F7479C37A9886B49FE5` + const registryPath = `SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes\` + packedUpgradeCode + + k, err := registry.OpenKey(registry.LOCAL_MACHINE, registryPath, registry.QUERY_VALUE) + if err == nil { + // If we can open the key, check if our executable is listed as an installed product under this upgrade code + // this means dbc is installed via MSI. + defer k.Close() + + // so check if we're running from the location where dbc.msi installs to + return strings.Contains(exe, `AppData\Roaming\Columnar\dbc`) + } + + if strings.HasSuffix(filepath.Dir(exe), "\\Scripts") { + // likely a pip install + return true + } + + return false +} diff --git a/cmd/dbc/main.go b/cmd/dbc/main.go index f6f4bccb..f7f4690c 100644 --- a/cmd/dbc/main.go +++ b/cmd/dbc/main.go @@ -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) diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md index 97cd0e09..3ce6e38f 100644 --- a/docs/getting_started/installation.md +++ b/docs/getting_started/installation.md @@ -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. diff --git a/drivers.go b/drivers.go index 231e0550..44ca0bac 100644 --- a/drivers.go +++ b/drivers.go @@ -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") + 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")) +} diff --git a/go.mod b/go.mod index 597ffa20..acdf162b 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 851cf605..749b2bb2 100644 --- a/go.sum +++ b/go.sum @@ -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=