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
2 changes: 1 addition & 1 deletion cmd/update/linux_asset_suffix_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

const (
linuxLdd235Suffix = "_ldd2-35"
linuxGlibcThreshold = "2.36"
linuxGlibcThreshold = "2.38"
)

var glibcVersionPattern = regexp.MustCompile(`(\d+\.\d+)(?:\.\d+)?`)
Expand Down
12 changes: 12 additions & 0 deletions cmd/update/linux_asset_suffix_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,22 @@ func TestLinuxAssetSuffixFromGlibcVersion(t *testing.T) {
want string
}{
{
// Ubuntu 22.04 (glibc 2.35) — cannot run the default glibc-2.38 binary.
output: "ldd (Ubuntu GLIBC 2.35-0ubuntu3.8) 2.35\n",
want: linuxLdd235Suffix,
},
{
// Debian 12 (glibc 2.36) — cannot run the default glibc-2.38 binary.
output: "ldd (Debian GLIBC 2.36-9+deb12u14) 2.36\n",
want: linuxLdd235Suffix,
},
{
// glibc 2.38 — the exact minimum required by the default binary.
output: "ldd (GNU libc) 2.38\n",
want: "",
},
{
// Ubuntu 24.04 (glibc 2.39) — can run the default binary.
output: "ldd (Ubuntu GLIBC 2.39-0ubuntu8.4) 2.39\n",
want: "",
},
Expand Down
69 changes: 69 additions & 0 deletions cmd/update/replace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package update

import (
"bytes"
"fmt"
"os"
"os/exec"
osruntime "runtime"
"strings"

"github.com/smartcontractkit/cre-cli/internal/ui"
)

const backupSuffix = ".bak"

func verifyBinaryRuns(binPath string) error {
cmd := exec.Command(binPath, "version") // #nosec G204 -- binPath is a verified release artifact
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
if msg := strings.TrimSpace(stderr.String()); msg != "" {
return fmt.Errorf("%s", msg)
}
return err
}
return nil
}

func replaceSelf(newBin string) error {
self, err := os.Executable()
if err != nil {
return err
}
return replaceBinaryAt(self, newBin)
}

func replaceBinaryAt(self, newBin string) error {
if osruntime.GOOS == "windows" {
ui.Warning("Automatic replacement not supported on Windows")
ui.Dim("Please close all running cre processes and manually replace the binary at:")
ui.Code(self)
ui.Dim("New binary downloaded at:")
ui.Code(newBin)
return fmt.Errorf("automatic replacement not supported on Windows")
}

backupPath := self + backupSuffix
_ = os.Remove(backupPath)

if err := os.Rename(self, backupPath); err != nil {
return fmt.Errorf("failed to backup current binary: %w", err)
}

if err := os.Rename(newBin, self); err != nil {
_ = os.Rename(backupPath, self)
return fmt.Errorf("failed to replace binary: %w", err)
}

if err := verifyBinaryRuns(self); err != nil {
_ = os.Remove(self)
if restoreErr := os.Rename(backupPath, self); restoreErr != nil {
return fmt.Errorf("post-install verification failed and could not restore backup: %v (verify error: %w)", restoreErr, err)
}
return fmt.Errorf("post-install verification failed; restored previous binary: %w", err)
}

_ = os.Remove(backupPath)
return nil
}
95 changes: 95 additions & 0 deletions cmd/update/replace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package update

import (
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"

"github.com/stretchr/testify/require"
)

func TestVerifyBinaryRuns(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell scripts used for unix fake binaries")
}
t.Parallel()

tmpDir := t.TempDir()
binPath := filepath.Join(tmpDir, "cre-test")
script := "#!/bin/sh\nexit 0\n"
require.NoError(t, os.WriteFile(binPath, []byte(script), 0755))

require.NoError(t, verifyBinaryRuns(binPath))

failPath := filepath.Join(tmpDir, "cre-fail")
failScript := "#!/bin/sh\necho glibc error >&2\nexit 1\n"
require.NoError(t, os.WriteFile(failPath, []byte(failScript), 0755))

err := verifyBinaryRuns(failPath)
require.Error(t, err)
require.Contains(t, err.Error(), "glibc error")
}

func TestReplaceBinaryAt_restoresBackupOnPostInstallFailure(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("unix-only replace flow")
}

tmpDir := t.TempDir()
current := filepath.Join(tmpDir, "cre")
newBin := filepath.Join(tmpDir, "cre-new")

currentScript := "#!/bin/sh\necho old\n"
require.NoError(t, os.WriteFile(current, []byte(currentScript), 0755))

newScript := "#!/bin/sh\necho broken >&2\nexit 1\n"
require.NoError(t, os.WriteFile(newBin, []byte(newScript), 0755))

err := replaceBinaryAt(current, newBin)
require.Error(t, err)
require.Contains(t, err.Error(), "restored previous binary")

restored, err := os.ReadFile(current)
require.NoError(t, err)
require.Equal(t, currentScript, string(restored))
require.NoFileExists(t, current+backupSuffix)
}

func TestReplaceBinaryAt_succeedsWithWorkingBinary(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("unix-only replace flow")
}

tmpDir := t.TempDir()
current := filepath.Join(tmpDir, "cre")
newBin := filepath.Join(tmpDir, "cre-new")

currentScript := "#!/bin/sh\necho old\n"
require.NoError(t, os.WriteFile(current, []byte(currentScript), 0755))

newScript := "#!/bin/sh\necho new\n"
require.NoError(t, os.WriteFile(newBin, []byte(newScript), 0755))

require.NoError(t, replaceBinaryAt(current, newBin))

updated, err := os.ReadFile(current)
require.NoError(t, err)
require.Equal(t, newScript, string(updated))
require.NoFileExists(t, current+backupSuffix)
require.NoFileExists(t, newBin)
}

func TestVerifyBinaryRuns_realGoBinary(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell scripts used for unix fake binaries")
}

tmpDir := t.TempDir()
binPath := filepath.Join(tmpDir, "version-bin")
out, err := exec.Command("go", "build", "-o", binPath, "testdata/version_main.go").CombinedOutput()
require.NoError(t, err, string(out))

require.NoError(t, verifyBinaryRuns(binPath))
}
7 changes: 7 additions & 0 deletions cmd/update/testdata/version_main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package main

import "fmt"

func main() {
fmt.Println("test-version")
}
30 changes: 11 additions & 19 deletions cmd/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,24 +282,6 @@ func unzip(assetPath string) (string, error) {
return "", errors.New("binary not found in zip")
}

func replaceSelf(newBin string) error {
self, err := os.Executable()
if err != nil {
return err
}
// On Windows, need to move after process exit
if osruntime.GOOS == "windows" {
ui.Warning("Automatic replacement not supported on Windows")
ui.Dim("Please close all running cre processes and manually replace the binary at:")
ui.Code(self)
ui.Dim("New binary downloaded at:")
ui.Code(newBin)
return fmt.Errorf("automatic replacement not supported on Windows")
}
// On Unix, can replace in-place
return os.Rename(newBin, self)
}

// Run accepts the currentVersion string and a force flag that overrides the
// fail-closed behavior when versions cannot be compared.
func Run(currentVersion string, force bool) error {
Expand Down Expand Up @@ -395,11 +377,21 @@ func Run(currentVersion string, force bool) error {
return fmt.Errorf("release signature verification failed: %w", err)
}

spinner.Update("Installing...")
if err := os.Chmod(binPath, 0755); err != nil {
spinner.Stop()
return fmt.Errorf("failed to set permissions: %w", err)
}

spinner.Update("Verifying binary...")
if err := verifyBinaryRuns(binPath); err != nil {
spinner.Stop()
if osruntime.GOOS == "linux" {
return fmt.Errorf("pre-install binary verification failed: %w; try downloading the compatible build manually: cre_linux_amd64_ldd2-35", err)
}
return fmt.Errorf("pre-install binary verification failed: %w", err)
}

spinner.Update("Installing...")
if err := replaceSelf(binPath); err != nil {
spinner.Stop()
return fmt.Errorf("failed to replace binary: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion install/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ verify_release_binary() {
}

LINUX_LDD235_SUFFIX="_ldd2-35"
LINUX_GLIBC_THRESHOLD="2.36"
LINUX_GLIBC_THRESHOLD="2.38"

parse_glibc_version_from_ldd_output() {
local output=$1
Expand Down