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 }