Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for macOS universal binaries #41

Merged
merged 6 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,27 @@ Self-Update library for Github, Gitea and Gitlab hosted applications in Go

# Introduction

go-selfupdate detects the information of the latest release via a source provider and
`go-selfupdate` detects the information of the latest release via a source provider and
checks the current version. If a newer version than itself is detected, it downloads the released binary from
the source provider and replaces itself.

- Automatically detect the latest version of released binary on the source provider
- Retrieve the proper binary for the OS and arch where the binary is running
- Update the binary with rollback support on failure
- Tested on Linux, macOS and Windows
- Support for different versions of ARM architectures
- Support macOS universal binaries
- Many archive and compression formats are supported (zip, tar, gzip, xzip, bzip2)
- Support private repositories
- Support hash, signature validation

Two source providers are available:
Three source providers are available:
- GitHub
- Gitea
- Gitlab

This library started as a fork of https://github.com/rhysd/go-github-selfupdate. A few things have changed from the original implementation:
- don't expose an external semver.Version type, but provide the same functionality through the API: LessThan, Equal and GreaterThan
- don't expose an external `semver.Version` type, but provide the same functionality through the API: `LessThan`, `Equal` and `GreaterThan`
- use an interface to send logs (compatible with standard log.Logger)
- able to detect different ARM CPU architectures (the original library wasn't working on my different versions of raspberry pi)
- support for assets compressed with bzip2 (.bz2)
Expand Down
9 changes: 0 additions & 9 deletions arm.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,8 @@ package selfupdate

import (
"debug/buildinfo"
"os"
)

var goarm uint8

//nolint:gochecknoinits
func init() {
// avoid using runtime.goarm directly
goarm = getGOARM(os.Args[0])
}

func getGOARM(goBinary string) uint8 {
build, err := buildinfo.ReadFile(goBinary)
if err != nil {
Expand Down
14 changes: 8 additions & 6 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ package selfupdate

// Config represents the configuration of self-update.
type Config struct {
// Source where to load the releases from (example: GitHubSource)
// Source where to load the releases from (example: GitHubSource).
Source Source
// Validator represents types which enable additional validation of downloaded release.
Validator Validator
// Filters are regexp used to filter on specific assets for releases with multiple assets.
// An asset is selected if it matches any of those, in addition to the regular tag, os, arch, extensions.
// Please make sure that your filter(s) uniquely match an asset.
Filters []string
// OS is set to the value of runtime.GOOS by default, but you can force another value here
// OS is set to the value of runtime.GOOS by default, but you can force another value here.
OS string
// Arch is set to the value of runtime.GOARCH by default, but you can force another value here
// Arch is set to the value of runtime.GOARCH by default, but you can force another value here.
Arch string
// Arm 32bits version. Valid values are 0 (unknown), 5, 6 or 7. Default is detected value (if any)
// Arm 32bits version. Valid values are 0 (unknown), 5, 6 or 7. Default is detected value (if available).
Arm uint8
// Draft permits an upgrade to a "draft" version (default to false)
// Arch name to use when using a universal binary (macOS only). Default to none.
UniversalArch string
// Draft permits an upgrade to a "draft" version (default to false).
Draft bool
// Prerelease permits an upgrade to a "pre-release" version (default to false)
// Prerelease permits an upgrade to a "pre-release" version (default to false).
Prerelease bool
}
2 changes: 1 addition & 1 deletion detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var reVersion = regexp.MustCompile(`\d+\.\d+\.\d+`)
// It fetches releases information from the source provider and find out the latest release with matching the tag names and asset names.
// Drafts and pre-releases are ignored.
// Assets would be suffixed by the OS name and the arch name such as 'foo_linux_amd64' where 'foo' is a command name.
// '-' can also be used as a separator. File can be compressed with zip, gzip, zxip, bzip2, tar&gzip or tar&zxip.
// '-' can also be used as a separator. File can be compressed with zip, gzip, xz, bzip2, tar&gzip or tar&xz.
// So the asset can have a file extension for the corresponding compression format such as '.zip'.
// On Windows, '.exe' also can be contained such as 'foo_windows_amd64.exe.zip'.
func (up *Updater) DetectLatest(ctx context.Context, repository Repository) (release *Release, found bool, err error) {
Expand Down
21 changes: 21 additions & 0 deletions internal/path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package internal

import (
"os"
"path/filepath"
)

// GetExecutablePath returns the path of the executable file with all symlinks resolved.
func GetExecutablePath() (string, error) {
exe, err := os.Executable()
if err != nil {
return "", err
}

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

return exe, nil
}
15 changes: 15 additions & 0 deletions internal/path_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package internal
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved

import (
"testing"

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

func TestGetExecutablePath(t *testing.T) {
t.Parallel()

exe, err := GetExecutablePath()
assert.NoError(t, err)
assert.NotEmpty(t, exe)
}
3 changes: 2 additions & 1 deletion update.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"strings"

"github.com/Masterminds/semver/v3"
"github.com/creativeprojects/go-selfupdate/internal"
"github.com/creativeprojects/go-selfupdate/update"
)

Expand Down Expand Up @@ -82,7 +83,7 @@
// UpdateSelf updates the running executable itself to the latest version.
// 'current' is used to check the latest version against the current version.
func (up *Updater) UpdateSelf(ctx context.Context, current string, repository Repository) (*Release, error) {
cmdPath, err := os.Executable()
cmdPath, err := internal.GetExecutablePath()

Check warning on line 86 in update.go

View check run for this annotation

Codecov / codecov/patch

update.go#L86

Added line #L86 was not covered by tests
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}
Expand Down
108 changes: 9 additions & 99 deletions update/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
import (
"bytes"
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
"os"
"path/filepath"

"github.com/creativeprojects/go-selfupdate/internal"
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
)

var (
Expand Down Expand Up @@ -37,7 +37,7 @@
// back to /path/to/target.
//
// If the roll back operation fails, the file system is left in an inconsistent state (between steps 5 and 6) where
// there is no new executable file and the old executable file could not be be moved to its original location. In this
// there is no new executable file and the old executable file could not be moved to its original location. In this
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
// case you should notify the user of the bad news and ask them to recover manually. Applications can determine whether
// the rollback failed by calling RollbackError, see the documentation on that function for additional detail.
func Apply(update io.Reader, opts Options) error {
Expand All @@ -61,14 +61,16 @@
opts.Verifier = NewECDSAVerifier()
}
if opts.TargetMode == 0 {
opts.TargetMode = 0755
opts.TargetMode = 0o755
}

// get target path
var err error
opts.TargetPath, err = opts.getPath()
if err != nil {
return err
if len(opts.TargetPath) == 0 {
opts.TargetPath, err = internal.GetExecutablePath()
if err != nil {
return err

Check warning on line 72 in update/apply.go

View check run for this annotation

Codecov / codecov/patch

update/apply.go#L70-L72

Added lines #L70 - L72 were not covered by tests
}
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
}

var newBytes []byte
Expand Down Expand Up @@ -180,95 +182,3 @@
error // original error
rollbackErr error // error encountered while rolling back
}

// Options for Apply update
type Options struct {
// TargetPath defines the path to the file to update.
// The emptry string means 'the executable file of the running program'.
TargetPath string

// Create TargetPath replacement with this file mode. If zero, defaults to 0755.
TargetMode os.FileMode

// Checksum of the new binary to verify against. If nil, no checksum or signature verification is done.
Checksum []byte

// Public key to use for signature verification. If nil, no signature verification is done.
PublicKey crypto.PublicKey

// Signature to verify the updated file. If nil, no signature verification is done.
Signature []byte

// Pluggable signature verification algorithm. If nil, ECDSA is used.
Verifier Verifier

// Use this hash function to generate the checksum. If not set, SHA256 is used.
Hash crypto.Hash

// Store the old executable file at this path after a successful update.
// The empty string means the old executable file will be removed after the update.
OldSavePath string
}

// SetPublicKeyPEM is a convenience method to set the PublicKey property
// used for checking a completed update's signature by parsing a
// Public Key formatted as PEM data.
func (o *Options) SetPublicKeyPEM(pembytes []byte) error {
block, _ := pem.Decode(pembytes)
if block == nil {
return errors.New("couldn't parse PEM data")
}

pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return err
}
o.PublicKey = pub
return nil
}

func (o *Options) getPath() (string, error) {
if o.TargetPath != "" {
return o.TargetPath, nil
}
exe, err := os.Executable()
if err != nil {
return "", err
}

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

return exe, nil
}

func (o *Options) verifyChecksum(updated []byte) error {
checksum, err := checksumFor(o.Hash, updated)
if err != nil {
return err
}

if !bytes.Equal(o.Checksum, checksum) {
return fmt.Errorf("updated file has wrong checksum. Expected: %x, got: %x", o.Checksum, checksum)
}
return nil
}

func (o *Options) verifySignature(updated []byte) error {
checksum, err := checksumFor(o.Hash, updated)
if err != nil {
return err
}
return o.Verifier.VerifySignature(checksum, o.Signature, o.Hash, o.PublicKey)
}

func checksumFor(h crypto.Hash, payload []byte) ([]byte, error) {
if !h.Available() {
return nil, errors.New("requested hash function not available")
}
hash := h.New()
hash.Write(payload) // guaranteed not to error
return hash.Sum([]byte{}), nil
}
Loading
Loading