Skip to content

Commit

Permalink
feat: native upx support (#3965)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
caarlos0 committed May 2, 2023
1 parent 57e104d commit 43ae761
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ builds:
universal_binaries:
- replace: false

upx:
- enabled: true

checksum:
name_template: 'checksums.txt'

Expand Down
116 changes: 116 additions & 0 deletions internal/pipe/upx/upx.go
Original file line number Diff line number Diff line change
@@ -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()
}
141 changes: 141 additions & 0 deletions internal/pipe/upx/upx_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
3 changes: 3 additions & 0 deletions internal/pipeline/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pkg/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -62,6 +63,7 @@ var Defaulters = []Defaulter{
gomod.Pipe{},
build.Pipe{},
universalbinary.Pipe{},
upx.Pipe{},
sourcearchive.Pipe{},
archive.Pipe{},
nfpm.Pipe{},
Expand Down
36 changes: 36 additions & 0 deletions www/docs/customization/upx.md
Original file line number Diff line number Diff line change
@@ -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
```
1 change: 1 addition & 0 deletions www/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 43ae761

Please sign in to comment.