1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-02-03 13:11:48 +02:00

feat: native upx support (#3965)

this adds a new root-level `upx` config, so users can pack their
binaries with upx :)

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
Carlos Alexandro Becker 2023-05-01 21:22:05 -03:00 committed by GitHub
parent 57e104d49c
commit 43ae761179
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 315 additions and 0 deletions

View File

@ -45,6 +45,8 @@ jobs:
sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft
mkdir -p $HOME/.cache/snapcraft/download
mkdir -p $HOME/.cache/snapcraft/stage-packages
- name: setup-upx
run: sudo apt-get -yq --no-install-suggests --no-install-recommends install upx-ucl
- uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4
with:
go-version: stable

View File

@ -42,6 +42,9 @@ builds:
universal_binaries:
- replace: false
upx:
- enabled: true
checksum:
name_template: 'checksums.txt'

116
internal/pipe/upx/upx.go Normal file
View File

@ -0,0 +1,116 @@
package upx
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/caarlos0/log"
"github.com/docker/go-units"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/pipe"
"github.com/goreleaser/goreleaser/internal/semerrgroup"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
)
type Pipe struct{}
func (Pipe) String() string { return "upx" }
func (Pipe) Default(ctx *context.Context) error {
for i := range ctx.Config.UPXs {
upx := &ctx.Config.UPXs[i]
if upx.Binary == "" {
upx.Binary = "upx"
}
}
return nil
}
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.UPXs) == 0 }
func (Pipe) Run(ctx *context.Context) error {
g := semerrgroup.NewSkipAware(semerrgroup.New(ctx.Parallelism))
for _, upx := range ctx.Config.UPXs {
upx := upx
if !upx.Enabled {
return pipe.Skip("upx is not enabled")
}
if _, err := exec.LookPath(upx.Binary); err != nil {
return pipe.Skipf("%s not found in PATH", upx.Binary)
}
for _, bin := range findBinaries(ctx, upx) {
bin := bin
g.Go(func() error {
sizeBefore := sizeOf(bin.Path)
args := []string{
"--quiet",
}
switch upx.Compress {
case "best":
args = append(args, "--best")
case "":
default:
args = append(args, "-"+upx.Compress)
}
if upx.LZMA {
args = append(args, "--lzma")
}
if upx.Brute {
args = append(args, "--brute")
}
args = append(args, bin.Path)
out, err := exec.CommandContext(ctx, "upx", args...).CombinedOutput()
if err != nil {
for _, ke := range knownExceptions {
if strings.Contains(string(out), ke) {
log.WithField("binary", bin.Path).
WithField("exception", ke).
Warn("could not pack")
return nil
}
}
return fmt.Errorf("could not pack %s: %w: %s", bin.Path, err, string(out))
}
sizeAfter := sizeOf(bin.Path)
log.
WithField("before", units.HumanSize(float64(sizeBefore))).
WithField("after", units.HumanSize(float64(sizeAfter))).
WithField("ratio", fmt.Sprintf("%d%%", (sizeAfter*100)/sizeBefore)).
WithField("binary", bin.Path).
Info("packed")
return nil
})
}
}
return g.Wait()
}
var knownExceptions = []string{
"CantPackException",
"AlreadyPackedException",
"NotCompressibleException",
}
func findBinaries(ctx *context.Context, upx config.UPX) []*artifact.Artifact {
filters := []artifact.Filter{
artifact.Or(
artifact.ByType(artifact.Binary),
artifact.ByType(artifact.UniversalBinary),
),
}
if len(upx.IDs) > 0 {
filters = append(filters, artifact.ByIDs(upx.IDs...))
}
return ctx.Artifacts.Filter(artifact.And(filters...)).List()
}
func sizeOf(name string) int64 {
st, err := os.Stat(name)
if err != nil {
return 0
}
return st.Size()
}

View File

@ -0,0 +1,141 @@
package upx
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/testctx"
"github.com/goreleaser/goreleaser/internal/testlib"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/stretchr/testify/require"
)
func TestStringer(t *testing.T) {
require.NotEmpty(t, Pipe{}.String())
}
func TestDefault(t *testing.T) {
ctx := testctx.NewWithCfg(config.Project{
UPXs: []config.UPX{
{},
},
})
require.NoError(t, Pipe{}.Default(ctx))
require.Len(t, ctx.Config.UPXs, 1)
require.Equal(t, "upx", ctx.Config.UPXs[0].Binary)
}
func TestSkip(t *testing.T) {
t.Run("skip", func(t *testing.T) {
ctx := testctx.NewWithCfg(config.Project{
UPXs: []config.UPX{},
})
require.True(t, Pipe{}.Skip(ctx))
})
t.Run("do not skip", func(t *testing.T) {
ctx := testctx.NewWithCfg(config.Project{
UPXs: []config.UPX{
{},
},
})
require.False(t, Pipe{}.Skip(ctx))
})
}
func TestRun(t *testing.T) {
ctx := testctx.NewWithCfg(config.Project{
UPXs: []config.UPX{
{
Enabled: true,
IDs: []string{"1"},
},
{
Enabled: true,
IDs: []string{"2"},
Compress: "best",
},
{
Enabled: true,
IDs: []string{"3"},
Compress: "9",
},
{
Enabled: true,
IDs: []string{"4"},
Compress: "8",
LZMA: true,
},
{
Enabled: true,
IDs: []string{"5"},
Brute: true,
},
},
})
tmp := t.TempDir()
main := filepath.Join(tmp, "main.go")
require.NoError(t, os.WriteFile(main, []byte("package main\nfunc main(){ println(1) }"), 0o644))
for _, goos := range []string{"linux", "windows", "darwin"} {
for _, goarch := range []string{"386", "amd64", "arm64"} {
ext := ""
if goos == "windows" {
ext = ".exe"
}
path := filepath.Join(tmp, fmt.Sprintf("bin_%s_%s%s", goos, goarch, ext))
cmd := exec.Command("go", "build", "-o", path, main)
cmd.Env = append([]string{
"CGO_ENABLED=0",
"GOOS=" + goos,
"GOARCH=" + goarch,
}, cmd.Environ()...)
if cmd.Run() != nil {
// ignore unsupported arches
continue
}
for i := 1; i <= 5; i++ {
ctx.Artifacts.Add(&artifact.Artifact{
Name: "bin",
Path: path,
Goos: goos,
Goarch: goarch,
Type: artifact.Binary,
Extra: map[string]any{
artifact.ExtraID: fmt.Sprintf("%d", i),
},
})
}
}
}
require.NoError(t, Pipe{}.Default(ctx))
require.NoError(t, Pipe{}.Run(ctx))
}
func TestDisabled(t *testing.T) {
ctx := testctx.NewWithCfg(config.Project{
UPXs: []config.UPX{
{},
},
})
testlib.AssertSkipped(t, Pipe{}.Run(ctx))
}
func TestUpxNotInstalled(t *testing.T) {
ctx := testctx.NewWithCfg(config.Project{
UPXs: []config.UPX{
{
Enabled: true,
Binary: "fakeupx",
},
},
})
testlib.AssertSkipped(t, Pipe{}.Run(ctx))
}

View File

@ -34,6 +34,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/snapshot"
"github.com/goreleaser/goreleaser/internal/pipe/sourcearchive"
"github.com/goreleaser/goreleaser/internal/pipe/universalbinary"
"github.com/goreleaser/goreleaser/internal/pipe/upx"
"github.com/goreleaser/goreleaser/pkg/context"
)
@ -74,6 +75,8 @@ var BuildPipeline = []Piper{
build.Pipe{},
// universal binary handling
universalbinary.Pipe{},
// upx
upx.Pipe{},
}
// BuildCmdPipeline is the pipeline run by goreleaser build.

View File

@ -517,6 +517,16 @@ type UniversalBinary struct {
Hooks BuildHookConfig `yaml:"hooks,omitempty" json:"hooks,omitempty"`
}
// UPX allows to compress binaries with `upx`.
type UPX struct {
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
IDs []string `yaml:"ids,omitempty" json:"ids,omitempty"`
Binary string `yaml:"binary,omitempty" json:"binary,omitempty"`
Compress string `yaml:"compress,omitempty" json:"compress,omitempty" jsonschema:"enum=1,enum=2,enum=3,enum=4,enum=5,enum=6,enum=7,enum=8,enum=9,enum=best,enum=,default="`
LZMA bool `yaml:"lzma,omitempty" json:"lzma,omitempty"`
Brute bool `yaml:"brute,omitempty" json:"brute,omitempty"`
}
// Archive config used for the archive.
type Archive struct {
ID string `yaml:"id,omitempty" json:"id,omitempty"`
@ -987,6 +997,7 @@ type Project struct {
ReportSizes bool `yaml:"report_sizes,omitempty" json:"report_sizes,omitempty"`
UniversalBinaries []UniversalBinary `yaml:"universal_binaries,omitempty" json:"universal_binaries,omitempty"`
UPXs []UPX `yaml:"upx,omitempty" json:"upx,omitempty"`
// this is a hack ¯\_(ツ)_/¯
SingleBuild Build `yaml:"build,omitempty" json:"build,omitempty" jsonschema_description:"deprecated: use builds instead"` // deprecated

View File

@ -40,6 +40,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/twitter"
"github.com/goreleaser/goreleaser/internal/pipe/universalbinary"
"github.com/goreleaser/goreleaser/internal/pipe/upload"
"github.com/goreleaser/goreleaser/internal/pipe/upx"
"github.com/goreleaser/goreleaser/internal/pipe/webhook"
"github.com/goreleaser/goreleaser/pkg/context"
)
@ -62,6 +63,7 @@ var Defaulters = []Defaulter{
gomod.Pipe{},
build.Pipe{},
universalbinary.Pipe{},
upx.Pipe{},
sourcearchive.Pipe{},
archive.Pipe{},
nfpm.Pipe{},

View File

@ -0,0 +1,36 @@
# UPX
> Since: v1.18
Having small binary sizes are important, and Go is known for generating rather
big binaries.
GoReleaser has had `-s -w` as default `ldflags` since the beginning, which help
shaving off some bytes, but if you want to shave it even more, [`upx`][upx] is
the _de facto_ tool for the job.
[upx]: https://upx.github.io/
GoReleaser has been able to integrate with it via custom [build hooks][bhooks],
and now UPX has its own configuration section:
```yaml
# .goreleaser.yaml
upx:
-
# Whether to enable it or not.
enabled: true
# Filter by build ID.
ids: [ build1, build2 ]
# Compress argument.
# Valid options are from '1' (faster) to '9' (better), and 'best'.
compress: best
# Whether to try LZMA (slower).
lzma: true
# Whether to try all methods and filters (slow).
brute: true
```

View File

@ -105,6 +105,7 @@ nav:
- customization/verifiable_builds.md
- customization/monorepo.md
- customization/universalbinaries.md
- customization/upx.md
- customization/partial.md
- Packaging and Archiving:
- customization/archive.md