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