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:
parent
33d857e347
commit
3c98e86620
@ -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'
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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),
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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 }}
|
||||
|
20
internal/pipe/brew/testdata/TestRunPipeUniversalBinary.rb.golden
vendored
Normal file
20
internal/pipe/brew/testdata/TestRunPipeUniversalBinary.rb.golden
vendored
Normal 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
|
@ -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
|
||||
|
@ -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{
|
||||
|
38
internal/pipe/gofish/testdata/TestRunPipeUniversalBinary.lua.golden
vendored
Normal file
38
internal/pipe/gofish/testdata/TestRunPipeUniversalBinary.lua.golden
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
1
internal/pipe/universalbinary/testdata/fake/go.mod
vendored
Normal file
1
internal/pipe/universalbinary/testdata/fake/go.mod
vendored
Normal file
@ -0,0 +1 @@
|
||||
module fake
|
5
internal/pipe/universalbinary/testdata/fake/main.go
vendored
Normal file
5
internal/pipe/universalbinary/testdata/fake/main.go
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
println("hello")
|
||||
}
|
178
internal/pipe/universalbinary/universalbinary.go
Normal file
178
internal/pipe/universalbinary/universalbinary.go
Normal 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),
|
||||
)
|
||||
}
|
222
internal/pipe/universalbinary/universalbinary_test.go
Normal file
222
internal/pipe/universalbinary/universalbinary_test.go
Normal 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)
|
||||
}
|
@ -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.
|
||||
|
@ -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"`
|
||||
|
||||
|
@ -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{},
|
||||
|
46
www/docs/customization/universalbinaries.md
Normal file
46
www/docs/customization/universalbinaries.md
Normal 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.
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user