1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-07-15 01:34:21 +02:00
Files
.github
cmd
internal
archivefiles
artifact
builders
client
commitauthor
deprecate
exec
extrafiles
gio
git
golden
http
ids
logext
middleware
pipe
announce
archive
artifactory
aur
before
blob
brew
build
changelog
checksums
chocolatey
custompublishers
defaults
discord
dist
docker
effectiveconfig
env
git
gomod
ko
krew
linkedin
mastodon
mattermost
metadata
milestone
nfpm
nix
opencollective
prebuild
project
publish
reddit
release
reportsizes
sbom
scoop
semver
sign
slack
smtp
snapcraft
snapshot
sourcearchive
teams
telegram
twitter
universalbinary
testdata
universalbinary.go
universalbinary_test.go
upload
upx
webhook
winget
pipe.go
pipe_test.go
pipeline
semerrgroup
shell
skips
static
testctx
testlib
tmpl
yaml
pkg
scripts
testdata
www
.editorconfig
.gitattributes
.gitignore
.gitleaks.toml
.golangci.yaml
.goreleaser-nightly.yaml
.goreleaser.yaml
.grype.yaml
.mailmap
CONTRIBUTING.md
Dockerfile
EULA.md
LICENSE.md
README.md
SECURITY.md
Taskfile.yml
USERS.md
art.txt
go.mod
go.sum
main.go
main_test.go
goreleaser/internal/pipe/universalbinary/universalbinary.go

263 lines
6.5 KiB
Go
Raw Normal View History

// 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/caarlos0/go-shellwords"
"github.com/caarlos0/log"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/gio"
"github.com/goreleaser/goreleaser/internal/ids"
"github.com/goreleaser/goreleaser/internal/pipe"
"github.com/goreleaser/goreleaser/internal/semerrgroup"
"github.com/goreleaser/goreleaser/internal/shell"
"github.com/goreleaser/goreleaser/internal/skips"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/build"
"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 len(unibin.IDs) == 0 {
unibin.IDs = []string{unibin.ID}
}
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 {
opts := build.Options{
Target: "darwin_all",
Goos: "darwin",
Goarch: "all",
}
2023-09-17 16:59:34 +00:00
if !skips.Any(ctx, skips.PreBuildHooks) {
if err := runHook(ctx, &opts, unibin.Hooks.Pre); err != nil {
return fmt.Errorf("pre hook failed: %w", err)
}
}
if err := makeUniversalBinary(ctx, &opts, unibin); err != nil {
return err
}
if !skips.Any(ctx, skips.PostBuildHooks) {
if err := runHook(ctx, &opts, unibin.Hooks.Post); err != nil {
return fmt.Errorf("post hook failed: %w", err)
}
}
if !unibin.Replace {
return nil
}
return ctx.Artifacts.Remove(filterFor(unibin))
})
}
return g.Wait()
}
func runHook(ctx *context.Context, opts *build.Options, hooks config.Hooks) error {
if len(hooks) == 0 {
return nil
}
for _, hook := range hooks {
var envs []string
envs = append(envs, ctx.Env.Strings()...)
tpl := tmpl.New(ctx).WithBuildOptions(*opts)
for _, rawEnv := range hook.Env {
env, err := tpl.Apply(rawEnv)
if err != nil {
return err
}
envs = append(envs, env)
}
tpl = tpl.WithEnvS(envs)
dir, err := tpl.Apply(hook.Dir)
if err != nil {
return err
}
sh, err := tpl.Apply(hook.Cmd)
if err != nil {
return err
}
log.WithField("hook", sh).Info("running hook")
cmd, err := shellwords.Parse(sh)
if err != nil {
return err
}
if err := shell.Run(ctx, dir, cmd, envs, hook.Output); err != nil {
return err
}
}
return nil
}
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, opts *build.Options, unibin config.UniversalBinary) error {
if err := tmpl.New(ctx).ApplyAll(
&unibin.NameTemplate,
&unibin.ModTimestamp,
); err != nil {
return err
}
name := unibin.NameTemplate
opts.Name = name
path := filepath.Join(ctx.Config.Dist, unibin.ID+"_darwin_all", name)
opts.Path = path
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
binaries := ctx.Artifacts.Filter(filterFor(unibin)).List()
if len(binaries) == 0 {
2023-03-22 23:49:48 -03:00
return pipe.Skipf("no darwin binaries found with id %q", unibin.ID)
}
log.WithField("id", unibin.ID).
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)
}
if err := gio.Chtimes(path, unibin.ModTimestamp); err != nil {
return err
}
extra := map[string]interface{}{}
for k, v := range binaries[0].Extra {
extra[k] = v
}
extra[artifact.ExtraReplaces] = unibin.Replace
extra[artifact.ExtraID] = unibin.ID
ctx.Artifacts.Add(&artifact.Artifact{
Type: artifact.UniversalBinary,
Name: name,
Path: path,
Goos: "darwin",
Goarch: "all",
Extra: extra,
})
return nil
}
func filterFor(unibin config.UniversalBinary) artifact.Filter {
return artifact.And(
artifact.ByType(artifact.Binary),
artifact.ByGoos("darwin"),
artifact.ByIDs(unibin.IDs...),
)
}