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 }