github.com/goreleaser/goreleaser@v1.25.1/internal/pipe/universalbinary/universalbinary.go (about)

     1  // Package universalbinary can join multiple darwin binaries into a single universal binary.
     2  package universalbinary
     3  
     4  import (
     5  	"debug/macho"
     6  	"encoding/binary"
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/caarlos0/go-shellwords"
    13  	"github.com/caarlos0/log"
    14  	"github.com/goreleaser/goreleaser/internal/artifact"
    15  	"github.com/goreleaser/goreleaser/internal/gio"
    16  	"github.com/goreleaser/goreleaser/internal/ids"
    17  	"github.com/goreleaser/goreleaser/internal/pipe"
    18  	"github.com/goreleaser/goreleaser/internal/semerrgroup"
    19  	"github.com/goreleaser/goreleaser/internal/shell"
    20  	"github.com/goreleaser/goreleaser/internal/skips"
    21  	"github.com/goreleaser/goreleaser/internal/tmpl"
    22  	"github.com/goreleaser/goreleaser/pkg/build"
    23  	"github.com/goreleaser/goreleaser/pkg/config"
    24  	"github.com/goreleaser/goreleaser/pkg/context"
    25  )
    26  
    27  // Pipe for macos universal binaries.
    28  type Pipe struct{}
    29  
    30  func (Pipe) String() string                 { return "universal binaries" }
    31  func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.UniversalBinaries) == 0 }
    32  
    33  // Default sets the pipe defaults.
    34  func (Pipe) Default(ctx *context.Context) error {
    35  	ids := ids.New("universal_binaries")
    36  	for i := range ctx.Config.UniversalBinaries {
    37  		unibin := &ctx.Config.UniversalBinaries[i]
    38  		if unibin.ID == "" {
    39  			unibin.ID = ctx.Config.ProjectName
    40  		}
    41  		if len(unibin.IDs) == 0 {
    42  			unibin.IDs = []string{unibin.ID}
    43  		}
    44  		if unibin.NameTemplate == "" {
    45  			unibin.NameTemplate = "{{ .ProjectName }}"
    46  		}
    47  		ids.Inc(unibin.ID)
    48  	}
    49  	return ids.Validate()
    50  }
    51  
    52  // Run the pipe.
    53  func (Pipe) Run(ctx *context.Context) error {
    54  	g := semerrgroup.NewSkipAware(semerrgroup.New(ctx.Parallelism))
    55  	for _, unibin := range ctx.Config.UniversalBinaries {
    56  		unibin := unibin
    57  		g.Go(func() error {
    58  			opts := build.Options{
    59  				Target: "darwin_all",
    60  				Goos:   "darwin",
    61  				Goarch: "all",
    62  			}
    63  			if !skips.Any(ctx, skips.PreBuildHooks) {
    64  				if err := runHook(ctx, &opts, unibin.Hooks.Pre); err != nil {
    65  					return fmt.Errorf("pre hook failed: %w", err)
    66  				}
    67  			}
    68  			if err := makeUniversalBinary(ctx, &opts, unibin); err != nil {
    69  				return err
    70  			}
    71  			if !skips.Any(ctx, skips.PostBuildHooks) {
    72  				if err := runHook(ctx, &opts, unibin.Hooks.Post); err != nil {
    73  					return fmt.Errorf("post hook failed: %w", err)
    74  				}
    75  			}
    76  			if !unibin.Replace {
    77  				return nil
    78  			}
    79  			return ctx.Artifacts.Remove(filterFor(unibin))
    80  		})
    81  	}
    82  	return g.Wait()
    83  }
    84  
    85  func runHook(ctx *context.Context, opts *build.Options, hooks config.Hooks) error {
    86  	if len(hooks) == 0 {
    87  		return nil
    88  	}
    89  
    90  	for _, hook := range hooks {
    91  		var envs []string
    92  		envs = append(envs, ctx.Env.Strings()...)
    93  
    94  		tpl := tmpl.New(ctx).WithBuildOptions(*opts)
    95  		for _, rawEnv := range hook.Env {
    96  			env, err := tpl.Apply(rawEnv)
    97  			if err != nil {
    98  				return err
    99  			}
   100  
   101  			envs = append(envs, env)
   102  		}
   103  
   104  		tpl = tpl.WithEnvS(envs)
   105  		dir, err := tpl.Apply(hook.Dir)
   106  		if err != nil {
   107  			return err
   108  		}
   109  
   110  		sh, err := tpl.Apply(hook.Cmd)
   111  		if err != nil {
   112  			return err
   113  		}
   114  
   115  		log.WithField("hook", sh).Info("running hook")
   116  		cmd, err := shellwords.Parse(sh)
   117  		if err != nil {
   118  			return err
   119  		}
   120  
   121  		if err := shell.Run(ctx, dir, cmd, envs, hook.Output); err != nil {
   122  			return err
   123  		}
   124  	}
   125  	return nil
   126  }
   127  
   128  type input struct {
   129  	data   []byte
   130  	cpu    uint32
   131  	subcpu uint32
   132  	offset int64
   133  }
   134  
   135  const (
   136  	// Alignment wanted for each sub-file.
   137  	// amd64 needs 12 bits, arm64 needs 14. We choose the max of all requirements here.
   138  	alignBits = 14
   139  	align     = 1 << alignBits
   140  )
   141  
   142  // heavily based on https://github.com/randall77/makefat
   143  func makeUniversalBinary(ctx *context.Context, opts *build.Options, unibin config.UniversalBinary) error {
   144  	if err := tmpl.New(ctx).ApplyAll(
   145  		&unibin.NameTemplate,
   146  		&unibin.ModTimestamp,
   147  	); err != nil {
   148  		return err
   149  	}
   150  	name := unibin.NameTemplate
   151  	opts.Name = name
   152  
   153  	path := filepath.Join(ctx.Config.Dist, unibin.ID+"_darwin_all", name)
   154  	opts.Path = path
   155  	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
   156  		return err
   157  	}
   158  
   159  	binaries := ctx.Artifacts.Filter(filterFor(unibin)).List()
   160  	if len(binaries) == 0 {
   161  		return pipe.Skipf("no darwin binaries found with ids: %s", strings.Join(unibin.IDs, ", "))
   162  	}
   163  
   164  	log.WithField("id", unibin.ID).
   165  		WithField("binary", path).
   166  		Infof("creating from %d binaries", len(binaries))
   167  
   168  	var inputs []input
   169  	offset := int64(align)
   170  	for _, f := range binaries {
   171  		data, err := os.ReadFile(f.Path)
   172  		if err != nil {
   173  			return fmt.Errorf("failed to read binary: %w", err)
   174  		}
   175  		inputs = append(inputs, input{
   176  			data:   data,
   177  			cpu:    binary.LittleEndian.Uint32(data[4:8]),
   178  			subcpu: binary.LittleEndian.Uint32(data[8:12]),
   179  			offset: offset,
   180  		})
   181  		offset += int64(len(data))
   182  		offset = (offset + align - 1) / align * align
   183  	}
   184  
   185  	// Make output file.
   186  	out, err := os.Create(path)
   187  	if err != nil {
   188  		return fmt.Errorf("failed to create file: %w", err)
   189  	}
   190  	defer out.Close()
   191  
   192  	if err := out.Chmod(0o755); err != nil {
   193  		return fmt.Errorf("failed to create file: %w", err)
   194  	}
   195  
   196  	// Build a fat_header.
   197  	hdr := []uint32{macho.MagicFat, uint32(len(inputs))}
   198  
   199  	// Build a fat_arch for each input file.
   200  	for _, i := range inputs {
   201  		hdr = append(hdr, i.cpu)
   202  		hdr = append(hdr, i.subcpu)
   203  		hdr = append(hdr, uint32(i.offset))
   204  		hdr = append(hdr, uint32(len(i.data)))
   205  		hdr = append(hdr, alignBits)
   206  	}
   207  
   208  	// Write header.
   209  	// Note that the fat binary header is big-endian, regardless of the
   210  	// endianness of the contained files.
   211  	if err := binary.Write(out, binary.BigEndian, hdr); err != nil {
   212  		return fmt.Errorf("failed to write to file: %w", err)
   213  	}
   214  	offset = int64(4 * len(hdr))
   215  
   216  	// Write each contained file.
   217  	for _, i := range inputs {
   218  		if offset < i.offset {
   219  			if _, err := out.Write(make([]byte, i.offset-offset)); err != nil {
   220  				return fmt.Errorf("failed to write to file: %w", err)
   221  			}
   222  			offset = i.offset
   223  		}
   224  		if _, err := out.Write(i.data); err != nil {
   225  			return fmt.Errorf("failed to write to file: %w", err)
   226  		}
   227  		offset += int64(len(i.data))
   228  	}
   229  
   230  	if err := out.Close(); err != nil {
   231  		return fmt.Errorf("failed to close file: %w", err)
   232  	}
   233  
   234  	if err := gio.Chtimes(path, unibin.ModTimestamp); err != nil {
   235  		return err
   236  	}
   237  
   238  	extra := map[string]interface{}{}
   239  	for k, v := range binaries[0].Extra {
   240  		extra[k] = v
   241  	}
   242  	extra[artifact.ExtraReplaces] = unibin.Replace
   243  	extra[artifact.ExtraID] = unibin.ID
   244  
   245  	ctx.Artifacts.Add(&artifact.Artifact{
   246  		Type:   artifact.UniversalBinary,
   247  		Name:   name,
   248  		Path:   path,
   249  		Goos:   "darwin",
   250  		Goarch: "all",
   251  		Extra:  extra,
   252  	})
   253  
   254  	return nil
   255  }
   256  
   257  func filterFor(unibin config.UniversalBinary) artifact.Filter {
   258  	return artifact.And(
   259  		artifact.ByType(artifact.Binary),
   260  		artifact.ByGoos("darwin"),
   261  		artifact.ByIDs(unibin.IDs...),
   262  	)
   263  }