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 3 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
12 changes: 7 additions & 5 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 All @@ -80,7 +82,7 @@ func update(version string) error {
return nil
}

exe, err := os.Executable()
exe, err := selfupdate.ExecutablePath()
if err != nil {
return errors.New("could not locate executable path")
}
Expand Down Expand Up @@ -353,7 +355,7 @@ func update() {
}
fmt.Printf("found release %s\n", release.Version())

exe, err := os.Executable()
exe, err := selfupdate.ExecutablePath()
if err != nil {
return errors.New("could not locate executable path")
}
Expand Down
17 changes: 12 additions & 5 deletions arch.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,24 @@ const (
maxARM = 7
)

// generateAdditionalArch we can use depending on the type of CPU
func generateAdditionalArch(arch string, goarm uint8) []string {
// getAdditionalArch we can use depending on the type of CPU
func getAdditionalArch(arch string, goarm uint8, universalArch string) []string {
if arch == "arm" && goarm >= minARM && goarm <= maxARM {
additionalArch := make([]string, 0, maxARM-minARM)
additionalArch := make([]string, 0, maxARM-minARM+1)
// more precise arch at the top of the list
for v := goarm; v >= minARM; v-- {
additionalArch = append(additionalArch, fmt.Sprintf("armv%d", v))
}
additionalArch = append(additionalArch, "arm")
return additionalArch
}
additionalArch := make([]string, 0, 3)
additionalArch = append(additionalArch, arch)
if arch == "amd64" {
return []string{"x86_64"}
additionalArch = append(additionalArch, "x86_64")
}
return []string{}
if universalArch != "" {
additionalArch = append(additionalArch, universalArch)
}
return additionalArch
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
}
27 changes: 15 additions & 12 deletions arch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,26 @@ import (

func TestAdditionalArch(t *testing.T) {
testData := []struct {
arch string
goarm uint8
expected []string
arch string
goarm uint8
universalArch string
expected []string
}{
{"arm64", 8, []string{}},
{"arm", 8, []string{}}, // armv8 is called arm64 - this shouldn't happen
{"arm", 7, []string{"armv7", "armv6", "armv5"}},
{"arm", 6, []string{"armv6", "armv5"}},
{"arm", 5, []string{"armv5"}},
{"arm", 4, []string{}}, // go is not supporting below armv5
{"amd64", 0, []string{"x86_64"}},
{"arm64", 0, "", []string{"arm64"}},
{"arm64", 0, "all", []string{"arm64", "all"}},
{"arm", 8, "", []string{"arm"}}, // armv8 is called arm64 - this shouldn't happen
{"arm", 7, "", []string{"armv7", "armv6", "armv5", "arm"}},
{"arm", 6, "", []string{"armv6", "armv5", "arm"}},
{"arm", 5, "", []string{"armv5", "arm"}},
{"arm", 4, "", []string{"arm"}}, // go is not supporting below armv5
{"amd64", 0, "", []string{"amd64", "x86_64"}},
{"amd64", 0, "all", []string{"amd64", "x86_64", "all"}},
}

for _, testItem := range testData {
t.Run(fmt.Sprintf("%s-%d", testItem.arch, testItem.goarm), func(t *testing.T) {
result := generateAdditionalArch(testItem.arch, testItem.goarm)
assert.ElementsMatch(t, testItem.expected, result)
result := getAdditionalArch(testItem.arch, testItem.goarm, testItem.universalArch)
assert.Equal(t, testItem.expected, result)
})
}
}
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
6 changes: 2 additions & 4 deletions cmd/detect-latest-release/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package main

import (
"context"
"errors"
"fmt"
"log"
"os"
"runtime"

"github.com/creativeprojects/go-selfupdate"
Expand All @@ -26,9 +24,9 @@ func update(version string) error {
return nil
}

exe, err := os.Executable()
exe, err := selfupdate.ExecutablePath()
creativeprojects marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return errors.New("could not locate executable path")
return fmt.Errorf("could not locate executable path: %w", err)
}
if err := selfupdate.UpdateTo(context.Background(), latest.AssetURL, latest.AssetName, exe); err != nil {
return fmt.Errorf("error occurred while updating binary: %w", err)
Expand Down
18 changes: 18 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
codecov:
notify:
after_n_builds: 6

comment:
after_n_builds: 6

coverage:
round: nearest
status:
project:
default:
target: auto
threshold: "2%"
patch:
default:
target: "70%"
threshold: "2%"
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
}
4 changes: 2 additions & 2 deletions 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 Expand Up @@ -131,7 +131,7 @@ func findValidationAsset(rel SourceRelease, validationName string) (SourceAsset,
func (up *Updater) findReleaseAndAsset(rels []SourceRelease, targetVersion string) (SourceRelease, SourceAsset, *semver.Version, bool) {
// we put the detected arch at the end of the list: that's fine for ARM so far,
// as the additional arch are more accurate than the generic one
for _, arch := range append(generateAdditionalArch(up.arch, up.arm), up.arch) {
for _, arch := range getAdditionalArch(up.arch, up.arm, up.universalArch) {
release, asset, version, found := up.findReleaseAndAssetForArch(arch, rels, targetVersion)
if found {
return release, asset, version, found
Expand Down
Loading