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