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:
parent
57e104d49c
commit
43ae761179
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -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
|
||||
|
@ -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
116
internal/pipe/upx/upx.go
Normal 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()
|
||||
}
|
141
internal/pipe/upx/upx_test.go
Normal file
141
internal/pipe/upx/upx_test.go
Normal 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))
|
||||
}
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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{},
|
||||
|
36
www/docs/customization/upx.md
Normal file
36
www/docs/customization/upx.md
Normal 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
|
||||
```
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user