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 }