1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-26 04:22:05 +02:00

feat: create macOS Universal binaries (#2572)

* feat: artifacts.Remove

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* feat: fatbinary

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* feat: run fatbinary on pipeline

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* feat: make archives work with fat binaries

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* feat: make brew work with fat binaries

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* feat: make gofish work with fat binaries

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* test: archive binary fatbin

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* docs: fat binaries

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* test: fix on linux

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* feat(ci): enable fat bins on goreleaser itself

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* refactor: rename to universal binaries

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* fix: config

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* fix: rename prop

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>
This commit is contained in:
Carlos Alexandro Becker 2021-10-12 14:55:43 -03:00 committed by GitHub
parent 33d857e347
commit 3c98e86620
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 785 additions and 17 deletions

View File

@ -33,6 +33,9 @@ builds:
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} -X main.builtBy=goreleaser
universal_binaries:
- replace: true
checksum:
name_template: 'checksums.txt'

View File

@ -30,6 +30,8 @@ const (
UploadableFile
// Binary is a binary (output of a gobuild).
Binary
// UniversalBinary is a binary that contains multiple binaries within.
UniversalBinary
// LinuxPackage is a linux package generated by nfpm.
LinuxPackage
// PublishableSnapcraft is a snap package yet to be published.
@ -62,7 +64,7 @@ func (t Type) String() string {
return "Archive"
case UploadableFile:
return "File"
case UploadableBinary, Binary:
case UploadableBinary, Binary, UniversalBinary:
return "Binary"
case LinuxPackage:
return "Linux Package"
@ -186,6 +188,32 @@ func (artifacts *Artifacts) Add(a *Artifact) {
artifacts.items = append(artifacts.items, a)
}
// Remove removes artifacts that match the given filter from the original artifact list.
func (artifacts *Artifacts) Remove(filter Filter) error {
if filter == nil {
return nil
}
artifacts.lock.Lock()
defer artifacts.lock.Unlock()
result := New()
for _, a := range artifacts.items {
if filter(a) {
log.WithFields(log.Fields{
"name": a.Name,
"path": a.Path,
"type": a.Type,
}).Debug("removing")
} else {
result.items = append(result.items, a)
}
}
artifacts.items = result.items
return nil
}
// Filter defines an artifact filter which can be used within the Filter
// function.
type Filter func(a *Artifact) bool

View File

@ -108,6 +108,59 @@ func TestFilter(t *testing.T) {
).List(), 2)
}
func TestRemove(t *testing.T) {
data := []*Artifact{
{
Name: "foo",
Goos: "linux",
Goarch: "arm",
Type: Binary,
},
{
Name: "universal",
Goos: "darwin",
Goarch: "all",
Type: UniversalBinary,
},
{
Name: "bar",
Goarch: "amd64",
},
{
Name: "checks",
Type: Checksum,
},
}
t.Run("null filter", func(t *testing.T) {
artifacts := New()
for _, a := range data {
artifacts.Add(a)
}
require.NoError(t, artifacts.Remove(nil))
require.Len(t, artifacts.List(), len(data))
})
t.Run("removing", func(t *testing.T) {
artifacts := New()
for _, a := range data {
artifacts.Add(a)
}
require.NoError(t, artifacts.Remove(
Or(
ByType(Checksum),
ByType(UniversalBinary),
And(
ByGoos("linux"),
ByGoarch("arm"),
),
),
))
require.Len(t, artifacts.List(), 1)
})
}
func TestGroupByPlatform(t *testing.T) {
data := []*Artifact{
{
@ -297,6 +350,7 @@ func TestTypeToString(t *testing.T) {
UploadableBinary,
UploadableFile,
Binary,
UniversalBinary,
LinuxPackage,
PublishableSnapcraft,
Snapcraft,

View File

@ -91,7 +91,10 @@ func (Pipe) Run(ctx *context.Context) error {
archive := archive
artifacts := ctx.Artifacts.Filter(
artifact.And(
artifact.ByType(artifact.Binary),
artifact.Or(
artifact.ByType(artifact.Binary),
artifact.ByType(artifact.UniversalBinary),
),
artifact.ByIDs(archive.Builds...),
),
).GroupByPlatform()

View File

@ -40,7 +40,7 @@ func TestRunPipe(t *testing.T) {
t.Run("Archive format "+format, func(t *testing.T) {
dist := filepath.Join(folder, format+"_dist")
require.NoError(t, os.Mkdir(dist, 0o755))
for _, arch := range []string{"darwinamd64", "linux386", "linuxarm7", "linuxmipssoftfloat"} {
for _, arch := range []string{"darwinamd64", "darwinall", "linux386", "linuxarm7", "linuxmipssoftfloat"} {
createFakeBinary(t, dist, arch, "bin/mybin")
}
createFakeBinary(t, dist, "windowsamd64", "bin/mybin.exe")
@ -76,6 +76,17 @@ func TestRunPipe(t *testing.T) {
},
},
)
darwinUniversalBinary := &artifact.Artifact{
Goos: "darwin",
Goarch: "all",
Name: "bin/mybin",
Path: filepath.Join(dist, "darwinall", "bin", "mybin"),
Type: artifact.Binary,
Extra: map[string]interface{}{
"Binary": "bin/mybin",
"ID": "default",
},
}
darwinBuild := &artifact.Artifact{
Goos: "darwin",
Goarch: "amd64",
@ -135,6 +146,7 @@ func TestRunPipe(t *testing.T) {
},
}
ctx.Artifacts.Add(darwinBuild)
ctx.Artifacts.Add(darwinUniversalBinary)
ctx.Artifacts.Add(linux386Build)
ctx.Artifacts.Add(linuxArmBuild)
ctx.Artifacts.Add(linuxMipsBuild)
@ -148,13 +160,14 @@ func TestRunPipe(t *testing.T) {
require.Equal(t, "myid", arch.Extra["ID"].(string), "all archives must have the archive ID set")
require.NotEmpty(t, arch.ExtraOr("Binaries", []string{}).([]string), "all archives must have the binary names they contain set")
}
require.Len(t, archives, 5)
require.Len(t, archives, 6)
// TODO: should verify the artifact fields here too
if format == "tar.gz" {
// Check archive contents
for name, os := range map[string]string{
"foobar_0.0.1_darwin_amd64.tar.gz": "darwin",
"foobar_0.0.1_darwin_all.tar.gz": "darwin",
"foobar_0.0.1_linux_386.tar.gz": "linux",
"foobar_0.0.1_linux_armv7.tar.gz": "linux",
"foobar_0.0.1_linux_mips_softfloat.tar.gz": "linux",
@ -346,6 +359,17 @@ func TestRunPipeBinary(t *testing.T) {
"ID": "default",
},
})
ctx.Artifacts.Add(&artifact.Artifact{
Goos: "darwin",
Goarch: "all",
Name: "myunibin",
Path: filepath.Join(dist, "darwinamd64", "mybin"),
Type: artifact.Binary,
Extra: map[string]interface{}{
"Binary": "myunibin",
"ID": "default",
},
})
ctx.Artifacts.Add(&artifact.Artifact{
Goos: "windows",
Goarch: "amd64",
@ -360,11 +384,19 @@ func TestRunPipeBinary(t *testing.T) {
})
require.NoError(t, Pipe{}.Run(ctx))
binaries := ctx.Artifacts.Filter(artifact.ByType(artifact.UploadableBinary))
darwin := binaries.Filter(artifact.ByGoos("darwin")).List()[0]
require.Len(t, binaries.List(), 3)
darwinThin := binaries.Filter(artifact.And(
artifact.ByGoos("darwin"),
artifact.ByGoarch("amd64"),
)).List()[0]
darwinUniversal := binaries.Filter(artifact.And(
artifact.ByGoos("darwin"),
artifact.ByGoarch("all"),
)).List()[0]
windows := binaries.Filter(artifact.ByGoos("windows")).List()[0]
require.Equal(t, "mybin_0.0.1_darwin_amd64", darwin.Name)
require.Equal(t, "mybin_0.0.1_darwin_amd64", darwinThin.Name)
require.Equal(t, "myunibin_0.0.1_darwin_all", darwinUniversal.Name)
require.Equal(t, "mybin_0.0.1_windows_amd64.exe", windows.Name)
require.Len(t, binaries.List(), 2)
}
func TestRunPipeDistRemoved(t *testing.T) {

View File

@ -156,6 +156,7 @@ func doRun(ctx *context.Context, brew config.Homebrew, cl client.Client) error {
artifact.Or(
artifact.ByGoarch("amd64"),
artifact.ByGoarch("arm64"),
artifact.ByGoarch("all"),
artifact.And(
artifact.ByGoarch("arm"),
artifact.ByGoarm(brew.Goarm),

View File

@ -978,3 +978,57 @@ func TestInstalls(t *testing.T) {
))
})
}
func TestRunPipeUniversalBinary(t *testing.T) {
folder := t.TempDir()
ctx := &context.Context{
Git: context.GitInfo{
CurrentTag: "v1.0.1",
},
Version: "1.0.1",
Artifacts: artifact.New(),
Config: config.Project{
Dist: folder,
ProjectName: "unibin",
Brews: []config.Homebrew{
{
Name: "unibin",
Tap: config.RepoRef{
Owner: "unibin",
Name: "bar",
},
IDs: []string{
"unibin",
},
Install: `bin.install "unibin"`,
},
},
},
}
path := filepath.Join(folder, "bin.tar.gz")
ctx.Artifacts.Add(&artifact.Artifact{
Name: "bin.tar.gz",
Path: path,
Goos: "darwin",
Goarch: "all",
Type: artifact.UploadableArchive,
Extra: map[string]interface{}{
"ID": "unibin",
"Format": "tar.gz",
},
})
f, err := os.Create(path)
require.NoError(t, err)
require.NoError(t, f.Close())
client := client.NewMock()
distFile := filepath.Join(folder, "unibin.rb")
require.NoError(t, runAll(ctx, client))
require.NoError(t, publishAll(ctx, client))
require.True(t, client.CreatedFile)
golden.RequireEqualRb(t, []byte(client.Content))
distBts, err := os.ReadFile(distFile)
require.NoError(t, err)
require.Equal(t, client.Content, string(distBts))
}

View File

@ -55,6 +55,11 @@ class {{ .Name }} < Formula
{{- if .MacOSPackages }}
on_macos do
{{- range $element := .MacOSPackages }}
{{- if eq $element.Arch "all" }}
url "{{ $element.DownloadURL }}"
{{- if .DownloadStrategy }}, :using => {{ .DownloadStrategy }}{{- end }}
sha256 "{{ $element.SHA256 }}"
{{- else }}
{{- if eq $element.Arch "amd64" }}
if Hardware::CPU.intel?
{{- end }}
@ -65,6 +70,7 @@ class {{ .Name }} < Formula
{{- if .DownloadStrategy }}, :using => {{ .DownloadStrategy }}{{- end }}
sha256 "{{ $element.SHA256 }}"
end
{{- end }}
{{- end }}
end
{{- end }}

View File

@ -0,0 +1,20 @@
# typed: false
# frozen_string_literal: true
# This file was generated by GoReleaser. DO NOT EDIT.
class Unibin < Formula
desc ""
homepage ""
version "1.0.1"
bottle :unneeded
depends_on :macos
on_macos do
url "https://dummyhost/download/v1.0.1/bin.tar.gz"
sha256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
end
def install
bin.install "unibin"
end
end

View File

@ -91,6 +91,7 @@ func doRun(ctx *context.Context, goFish config.GoFish, cl client.Client) error {
artifact.Or(
artifact.ByGoarch("amd64"),
artifact.ByGoarch("arm64"),
artifact.ByGoarch("all"),
artifact.And(
artifact.ByGoarch("arm"),
artifact.ByGoarm(goFish.Goarm),
@ -206,19 +207,27 @@ func dataFor(ctx *context.Context, cfg config.GoFish, cl client.Client, artifact
if err != nil {
return result, err
}
releasePackage := releasePackage{
DownloadURL: url,
SHA256: sum,
OS: artifact.Goos,
Arch: artifact.Goarch,
Binaries: artifact.ExtraOr("Binaries", []string{}).([]string),
goarch := []string{artifact.Goarch}
if artifact.Goarch == "all" {
goarch = []string{"amd64", "arm64"}
}
for _, v := range result.ReleasePackages {
if v.OS == artifact.Goos && v.Arch == artifact.Goarch {
return result, ErrMultipleArchivesSameOS
for _, arch := range goarch {
releasePackage := releasePackage{
DownloadURL: url,
SHA256: sum,
OS: artifact.Goos,
Arch: arch,
Binaries: artifact.ExtraOr("Binaries", []string{}).([]string),
}
for _, v := range result.ReleasePackages {
if v.OS == artifact.Goos && v.Arch == artifact.Goarch {
return result, ErrMultipleArchivesSameOS
}
}
result.ReleasePackages = append(result.ReleasePackages, releasePackage)
}
result.ReleasePackages = append(result.ReleasePackages, releasePackage)
}
return result, nil

View File

@ -237,6 +237,60 @@ func TestFullPipe(t *testing.T) {
}
}
func TestRunPipeUniversalBinary(t *testing.T) {
folder := t.TempDir()
ctx := &context.Context{
Git: context.GitInfo{
CurrentTag: "v1.0.1",
},
Version: "1.0.1",
Artifacts: artifact.New(),
Config: config.Project{
Dist: folder,
ProjectName: "unibin",
Rigs: []config.GoFish{
{
Name: "unibin",
Rig: config.RepoRef{
Owner: "unibin",
Name: "bar",
},
IDs: []string{
"unibin",
},
},
},
},
}
path := filepath.Join(folder, "bin.tar.gz")
ctx.Artifacts.Add(&artifact.Artifact{
Name: "unibin.tar.gz",
Path: path,
Goos: "darwin",
Goarch: "all",
Type: artifact.UploadableArchive,
Extra: map[string]interface{}{
"ID": "unibin",
"Format": "tar.gz",
"Binaries": []string{"unibin"},
},
})
f, err := os.Create(path)
require.NoError(t, err)
require.NoError(t, f.Close())
client := client.NewMock()
distFile := filepath.Join(folder, "unibin.lua")
require.NoError(t, runAll(ctx, client))
require.NoError(t, publishAll(ctx, client))
require.True(t, client.CreatedFile)
golden.RequireEqualLua(t, []byte(client.Content))
distBts, err := os.ReadFile(distFile)
require.NoError(t, err)
require.Equal(t, client.Content, string(distBts))
}
func TestRunPipeNameTemplate(t *testing.T) {
folder := t.TempDir()
ctx := &context.Context{

View File

@ -0,0 +1,38 @@
local name = "unibin"
local version = "1.0.1"
food = {
name = name,
description = "",
license = "",
homepage = "",
version = version,
packages = {
{
os = "darwin",
arch = "amd64",
url = "https://dummyhost/download/v1.0.1/unibin.tar.gz",
sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
resources = {
{
path = "unibin",
installpath = "bin/unibin",
executable = true
}
}
},
{
os = "darwin",
arch = "arm64",
url = "https://dummyhost/download/v1.0.1/unibin.tar.gz",
sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
resources = {
{
path = "unibin",
installpath = "bin/unibin",
executable = true
}
}
},
}
}

View File

@ -0,0 +1 @@
module fake

View File

@ -0,0 +1,5 @@
package main
func main() {
println("hello")
}

View File

@ -0,0 +1,178 @@
// Package universalbinary can join multiple darwin binaries into a single universal binary.
package universalbinary
import (
"debug/macho"
"encoding/binary"
"fmt"
"os"
"path/filepath"
"github.com/apex/log"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/ids"
"github.com/goreleaser/goreleaser/internal/pipe"
"github.com/goreleaser/goreleaser/internal/semerrgroup"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
)
// Pipe for macos universal binaries.
type Pipe struct{}
func (Pipe) String() string { return "universal binaries" }
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.UniversalBinaries) == 0 }
// Default sets the pipe defaults.
func (Pipe) Default(ctx *context.Context) error {
ids := ids.New("universal_binaries")
for i := range ctx.Config.UniversalBinaries {
unibin := &ctx.Config.UniversalBinaries[i]
if unibin.ID == "" {
unibin.ID = ctx.Config.ProjectName
}
if unibin.NameTemplate == "" {
unibin.NameTemplate = "{{ .ProjectName }}"
}
ids.Inc(unibin.ID)
}
return ids.Validate()
}
// Run the pipe.
func (Pipe) Run(ctx *context.Context) error {
g := semerrgroup.NewSkipAware(semerrgroup.New(ctx.Parallelism))
for _, unibin := range ctx.Config.UniversalBinaries {
unibin := unibin
g.Go(func() error {
if err := makeUniversalBinary(ctx, unibin); err != nil {
return err
}
if !unibin.Replace {
return nil
}
return ctx.Artifacts.Remove(filterFor(unibin))
})
}
return g.Wait()
}
type input struct {
data []byte
cpu uint32
subcpu uint32
offset int64
}
const (
// Alignment wanted for each sub-file.
// amd64 needs 12 bits, arm64 needs 14. We choose the max of all requirements here.
alignBits = 14
align = 1 << alignBits
)
// heavily based on https://github.com/randall77/makefat
func makeUniversalBinary(ctx *context.Context, unibin config.UniversalBinary) error {
name, err := tmpl.New(ctx).Apply(unibin.NameTemplate)
if err != nil {
return err
}
path := filepath.Join(ctx.Config.Dist, name+"_darwinall", name)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
binaries := ctx.Artifacts.Filter(filterFor(unibin)).List()
if len(binaries) == 0 {
return pipe.Skip(fmt.Sprintf("no darwin binaries found with id %q", unibin.ID))
}
log.WithField("binary", path).Infof("creating from %d binaries", len(binaries))
var inputs []input
offset := int64(align)
for _, f := range binaries {
data, err := os.ReadFile(f.Path)
if err != nil {
return fmt.Errorf("failed to read binary: %w", err)
}
inputs = append(inputs, input{
data: data,
cpu: binary.LittleEndian.Uint32(data[4:8]),
subcpu: binary.LittleEndian.Uint32(data[8:12]),
offset: offset,
})
offset += int64(len(data))
offset = (offset + align - 1) / align * align
}
// Make output file.
out, err := os.Create(path)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer out.Close()
if err := out.Chmod(0o755); err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
// Build a fat_header.
hdr := []uint32{macho.MagicFat, uint32(len(inputs))}
// Build a fat_arch for each input file.
for _, i := range inputs {
hdr = append(hdr, i.cpu)
hdr = append(hdr, i.subcpu)
hdr = append(hdr, uint32(i.offset))
hdr = append(hdr, uint32(len(i.data)))
hdr = append(hdr, alignBits)
}
// Write header.
// Note that the fat binary header is big-endian, regardless of the
// endianness of the contained files.
if err := binary.Write(out, binary.BigEndian, hdr); err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
offset = int64(4 * len(hdr))
// Write each contained file.
for _, i := range inputs {
if offset < i.offset {
if _, err := out.Write(make([]byte, i.offset-offset)); err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
offset = i.offset
}
if _, err := out.Write(i.data); err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
offset += int64(len(i.data))
}
if err := out.Close(); err != nil {
return fmt.Errorf("failed to close file: %w", err)
}
ctx.Artifacts.Add(&artifact.Artifact{
Type: artifact.UniversalBinary,
Name: name,
Path: path,
Goos: "darwin",
Goarch: "all",
Extra: binaries[0].Extra,
})
return nil
}
func filterFor(unibin config.UniversalBinary) artifact.Filter {
return artifact.And(
artifact.ByType(artifact.Binary),
artifact.ByGoos("darwin"),
artifact.ByIDs(unibin.ID),
)
}

View File

@ -0,0 +1,222 @@
package universalbinary
import (
"debug/macho"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
"github.com/stretchr/testify/require"
)
func TestDescription(t *testing.T) {
require.NotEmpty(t, Pipe{}.String())
}
func TestDefault(t *testing.T) {
t.Run("empty", func(t *testing.T) {
ctx := &context.Context{
Config: config.Project{
ProjectName: "proj",
UniversalBinaries: []config.UniversalBinary{
{},
},
},
}
require.NoError(t, Pipe{}.Default(ctx))
require.Equal(t, config.UniversalBinary{
ID: "proj",
NameTemplate: "{{ .ProjectName }}",
}, ctx.Config.UniversalBinaries[0])
})
t.Run("given id", func(t *testing.T) {
ctx := &context.Context{
Config: config.Project{
ProjectName: "proj",
UniversalBinaries: []config.UniversalBinary{
{ID: "foo"},
},
},
}
require.NoError(t, Pipe{}.Default(ctx))
require.Equal(t, config.UniversalBinary{
ID: "foo",
NameTemplate: "{{ .ProjectName }}",
}, ctx.Config.UniversalBinaries[0])
})
t.Run("given name", func(t *testing.T) {
ctx := &context.Context{
Config: config.Project{
ProjectName: "proj",
UniversalBinaries: []config.UniversalBinary{
{NameTemplate: "foo"},
},
},
}
require.NoError(t, Pipe{}.Default(ctx))
require.Equal(t, config.UniversalBinary{
ID: "proj",
NameTemplate: "foo",
}, ctx.Config.UniversalBinaries[0])
})
t.Run("duplicated ids", func(t *testing.T) {
ctx := &context.Context{
Config: config.Project{
ProjectName: "proj",
UniversalBinaries: []config.UniversalBinary{
{ID: "foo"},
{ID: "foo"},
},
},
}
require.EqualError(t, Pipe{}.Default(ctx), `found 2 universal_binaries with the ID 'foo', please fix your config`)
})
}
func TestSkip(t *testing.T) {
t.Run("skip", func(t *testing.T) {
require.True(t, Pipe{}.Skip(context.New(config.Project{})))
})
t.Run("dont skip", func(t *testing.T) {
ctx := context.New(config.Project{
UniversalBinaries: []config.UniversalBinary{{}},
})
require.False(t, Pipe{}.Skip(ctx))
})
}
func TestRun(t *testing.T) {
dist := t.TempDir()
src := filepath.Join("testdata", "fake", "main.go")
paths := map[string]string{
"amd64": filepath.Join(dist, "fake_darwin_amd64/fake"),
"arm64": filepath.Join(dist, "fake_darwin_arm64/fake"),
}
ctx1 := context.New(config.Project{
Dist: dist,
UniversalBinaries: []config.UniversalBinary{
{
ID: "foo",
NameTemplate: "foo",
Replace: true,
},
},
})
ctx2 := context.New(config.Project{
Dist: dist,
UniversalBinaries: []config.UniversalBinary{
{
ID: "foo",
NameTemplate: "foo",
},
},
})
ctx3 := context.New(config.Project{
Dist: dist,
UniversalBinaries: []config.UniversalBinary{
{
ID: "notfoo",
NameTemplate: "notfoo",
},
},
})
ctx4 := context.New(config.Project{
Dist: dist,
UniversalBinaries: []config.UniversalBinary{
{
ID: "foo",
NameTemplate: "foo",
},
},
})
for arch, path := range paths {
cmd := exec.Command("go", "build", "-o", path, src)
cmd.Env = append(os.Environ(), "GOOS=darwin", "GOARCH="+arch)
out, err := cmd.CombinedOutput()
t.Log(string(out))
require.NoError(t, err)
modTime := time.Unix(0, 0)
require.NoError(t, os.Chtimes(path, modTime, modTime))
art := artifact.Artifact{
Name: "fake",
Path: path,
Goos: "darwin",
Goarch: arch,
Type: artifact.Binary,
Extra: map[string]interface{}{
"Binary": "fake",
"ID": "foo",
},
}
ctx1.Artifacts.Add(&art)
ctx2.Artifacts.Add(&art)
ctx4.Artifacts.Add(&artifact.Artifact{
Name: "fake",
Path: path + "wrong",
Goos: "darwin",
Goarch: arch,
Type: artifact.Binary,
Extra: map[string]interface{}{
"Binary": "fake",
"ID": "foo",
},
})
}
t.Run("replacing", func(t *testing.T) {
require.NoError(t, Pipe{}.Run(ctx1))
require.Len(t, ctx1.Artifacts.Filter(artifact.ByType(artifact.Binary)).List(), 0)
require.Len(t, ctx1.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List(), 1)
checkUniversalBinary(t, ctx1.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List()[0])
})
t.Run("keeping", func(t *testing.T) {
require.NoError(t, Pipe{}.Run(ctx2))
require.Len(t, ctx2.Artifacts.Filter(artifact.ByType(artifact.Binary)).List(), 2)
require.Len(t, ctx2.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List(), 1)
checkUniversalBinary(t, ctx2.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List()[0])
})
t.Run("bad template", func(t *testing.T) {
require.EqualError(t, Pipe{}.Run(context.New(config.Project{
UniversalBinaries: []config.UniversalBinary{
{
NameTemplate: "{{.Name}",
},
},
})), `template: tmpl:1: unexpected "}" in operand`)
})
t.Run("no darwin builds", func(t *testing.T) {
require.EqualError(t, Pipe{}.Run(ctx3), `no darwin binaries found with id "notfoo"`)
})
t.Run("fail to open", func(t *testing.T) {
require.ErrorIs(t, Pipe{}.Run(ctx4), os.ErrNotExist)
})
}
func checkUniversalBinary(tb testing.TB, unibin *artifact.Artifact) {
tb.Helper()
f, err := macho.OpenFat(unibin.Path)
require.NoError(tb, err)
require.Len(tb, f.Arches, 2)
}

View File

@ -27,6 +27,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/snapcraft"
"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/pkg/context"
)
@ -53,6 +54,7 @@ var BuildPipeline = []Piper{
effectiveconfig.Pipe{}, // writes the actual config (with defaults et al set) to dist
changelog.Pipe{}, // builds the release changelog
build.Pipe{}, // build
universalbinary.Pipe{}, // universal binary handling
}
// Pipeline contains all pipe implementations in order.

View File

@ -317,6 +317,13 @@ func (f *File) UnmarshalYAML(unmarshal func(interface{}) error) error {
return nil
}
// UniversalBinary setups macos universal binaries.
type UniversalBinary struct {
ID string `yaml:"id,omitempty"`
NameTemplate string `yaml:"name_template,omitempty"`
Replace bool `yaml:",omitempty"`
}
// Archive config used for the archive.
type Archive struct {
ID string `yaml:",omitempty"`
@ -689,6 +696,8 @@ type Project struct {
GoMod GoMod `yaml:"gomod,omitempty"`
Announce Announce `yaml:"announce,omitempty"`
UniversalBinaries []UniversalBinary `yaml:"universal_binaries,omitempty"`
// this is a hack ¯\_(ツ)_/¯
SingleBuild Build `yaml:"build,omitempty"`

View File

@ -31,6 +31,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/teams"
"github.com/goreleaser/goreleaser/internal/pipe/telegram"
"github.com/goreleaser/goreleaser/internal/pipe/twitter"
"github.com/goreleaser/goreleaser/internal/pipe/universalbinary"
"github.com/goreleaser/goreleaser/pkg/context"
)
@ -51,6 +52,7 @@ var Defaulters = []Defaulter{
project.Pipe{},
gomod.Pipe{},
build.Pipe{},
universalbinary.Pipe{},
sourcearchive.Pipe{},
archive.Pipe{},
nfpm.Pipe{},

View File

@ -0,0 +1,46 @@
---
title: MacOS Universal Binaries
---
GoReleaser can create _MacOS Universal Binaries_ - also known as _Fat Binaries_.
Those binaries are in a special format that contains both `arm64` and `amd64` executables in a single file.
Here's how to use it:
```yaml
# .goreleaser.yml
universal_binaries:
-
# ID of the source build
#
# Defaults to the project name.
id: foo
# Universal binary name template.
#
# Defaults to '{{ .ProjectName }}'
name_template: '{{.ProjectName}}_{{.Version}}'
# Whether to remove the previous single-arch binaries from the artifact list.
# If left as false, your end release might have both several macOS archives: amd64, arm64 and all.
#
# Defaults to false.
replace: true
```
!!! tip
Learn more about the [name template engine](/customization/templates/).
The minimal configuration for most setups would look like this:
```yaml
# .goreleaser.yml
universal_binaries:
- replace: true
```
That config will join your default build macOS binaries into an Universal Binary,
removing the single-arch binaries from the artifact list.
From there, the `Arch` template variable for this file will be `all`.
You can use the Go template engine to remove it if you'd like.

View File

@ -80,6 +80,7 @@ nav:
- customization/build.md
- customization/gomod.md
- customization/monorepo.md
- customization/universalbinaries.md
- Packaging and Archiving:
- customization/archive.md
- customization/nfpm.md