mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-04-15 11:56:56 +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
|
sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft
|
||||||
mkdir -p $HOME/.cache/snapcraft/download
|
mkdir -p $HOME/.cache/snapcraft/download
|
||||||
mkdir -p $HOME/.cache/snapcraft/stage-packages
|
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
|
- uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4
|
||||||
with:
|
with:
|
||||||
go-version: stable
|
go-version: stable
|
||||||
|
@ -42,6 +42,9 @@ builds:
|
|||||||
universal_binaries:
|
universal_binaries:
|
||||||
- replace: false
|
- replace: false
|
||||||
|
|
||||||
|
upx:
|
||||||
|
- enabled: true
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
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/snapshot"
|
||||||
"github.com/goreleaser/goreleaser/internal/pipe/sourcearchive"
|
"github.com/goreleaser/goreleaser/internal/pipe/sourcearchive"
|
||||||
"github.com/goreleaser/goreleaser/internal/pipe/universalbinary"
|
"github.com/goreleaser/goreleaser/internal/pipe/universalbinary"
|
||||||
|
"github.com/goreleaser/goreleaser/internal/pipe/upx"
|
||||||
"github.com/goreleaser/goreleaser/pkg/context"
|
"github.com/goreleaser/goreleaser/pkg/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -74,6 +75,8 @@ var BuildPipeline = []Piper{
|
|||||||
build.Pipe{},
|
build.Pipe{},
|
||||||
// universal binary handling
|
// universal binary handling
|
||||||
universalbinary.Pipe{},
|
universalbinary.Pipe{},
|
||||||
|
// upx
|
||||||
|
upx.Pipe{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildCmdPipeline is the pipeline run by goreleaser build.
|
// BuildCmdPipeline is the pipeline run by goreleaser build.
|
||||||
|
@ -517,6 +517,16 @@ type UniversalBinary struct {
|
|||||||
Hooks BuildHookConfig `yaml:"hooks,omitempty" json:"hooks,omitempty"`
|
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.
|
// Archive config used for the archive.
|
||||||
type Archive struct {
|
type Archive struct {
|
||||||
ID string `yaml:"id,omitempty" json:"id,omitempty"`
|
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"`
|
ReportSizes bool `yaml:"report_sizes,omitempty" json:"report_sizes,omitempty"`
|
||||||
|
|
||||||
UniversalBinaries []UniversalBinary `yaml:"universal_binaries,omitempty" json:"universal_binaries,omitempty"`
|
UniversalBinaries []UniversalBinary `yaml:"universal_binaries,omitempty" json:"universal_binaries,omitempty"`
|
||||||
|
UPXs []UPX `yaml:"upx,omitempty" json:"upx,omitempty"`
|
||||||
|
|
||||||
// this is a hack ¯\_(ツ)_/¯
|
// this is a hack ¯\_(ツ)_/¯
|
||||||
SingleBuild Build `yaml:"build,omitempty" json:"build,omitempty" jsonschema_description:"deprecated: use builds instead"` // deprecated
|
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/twitter"
|
||||||
"github.com/goreleaser/goreleaser/internal/pipe/universalbinary"
|
"github.com/goreleaser/goreleaser/internal/pipe/universalbinary"
|
||||||
"github.com/goreleaser/goreleaser/internal/pipe/upload"
|
"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/internal/pipe/webhook"
|
||||||
"github.com/goreleaser/goreleaser/pkg/context"
|
"github.com/goreleaser/goreleaser/pkg/context"
|
||||||
)
|
)
|
||||||
@ -62,6 +63,7 @@ var Defaulters = []Defaulter{
|
|||||||
gomod.Pipe{},
|
gomod.Pipe{},
|
||||||
build.Pipe{},
|
build.Pipe{},
|
||||||
universalbinary.Pipe{},
|
universalbinary.Pipe{},
|
||||||
|
upx.Pipe{},
|
||||||
sourcearchive.Pipe{},
|
sourcearchive.Pipe{},
|
||||||
archive.Pipe{},
|
archive.Pipe{},
|
||||||
nfpm.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/verifiable_builds.md
|
||||||
- customization/monorepo.md
|
- customization/monorepo.md
|
||||||
- customization/universalbinaries.md
|
- customization/universalbinaries.md
|
||||||
|
- customization/upx.md
|
||||||
- customization/partial.md
|
- customization/partial.md
|
||||||
- Packaging and Archiving:
|
- Packaging and Archiving:
|
||||||
- customization/archive.md
|
- customization/archive.md
|
||||||
|
Loading…
x
Reference in New Issue
Block a user