github.com/goreleaser/goreleaser@v1.25.1/internal/pipe/universalbinary/universalbinary_test.go (about) 1 package universalbinary 2 3 import ( 4 "debug/macho" 5 "fmt" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "strconv" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/goreleaser/goreleaser/internal/artifact" 15 "github.com/goreleaser/goreleaser/internal/skips" 16 "github.com/goreleaser/goreleaser/internal/testctx" 17 "github.com/goreleaser/goreleaser/internal/testlib" 18 "github.com/goreleaser/goreleaser/pkg/config" 19 "github.com/stretchr/testify/require" 20 ) 21 22 func TestDescription(t *testing.T) { 23 require.NotEmpty(t, Pipe{}.String()) 24 } 25 26 func TestDefault(t *testing.T) { 27 t.Run("empty", func(t *testing.T) { 28 ctx := testctx.NewWithCfg(config.Project{ 29 ProjectName: "proj", 30 UniversalBinaries: []config.UniversalBinary{ 31 {}, 32 }, 33 }) 34 require.NoError(t, Pipe{}.Default(ctx)) 35 require.Equal(t, config.UniversalBinary{ 36 ID: "proj", 37 IDs: []string{"proj"}, 38 NameTemplate: "{{ .ProjectName }}", 39 }, ctx.Config.UniversalBinaries[0]) 40 }) 41 42 t.Run("given ids", func(t *testing.T) { 43 ctx := testctx.NewWithCfg(config.Project{ 44 ProjectName: "proj", 45 UniversalBinaries: []config.UniversalBinary{ 46 {IDs: []string{"foo"}}, 47 }, 48 }) 49 require.NoError(t, Pipe{}.Default(ctx)) 50 require.Equal(t, config.UniversalBinary{ 51 ID: "proj", 52 IDs: []string{"foo"}, 53 NameTemplate: "{{ .ProjectName }}", 54 }, ctx.Config.UniversalBinaries[0]) 55 }) 56 57 t.Run("given id", func(t *testing.T) { 58 ctx := testctx.NewWithCfg(config.Project{ 59 ProjectName: "proj", 60 UniversalBinaries: []config.UniversalBinary{ 61 {ID: "foo"}, 62 }, 63 }) 64 require.NoError(t, Pipe{}.Default(ctx)) 65 require.Equal(t, config.UniversalBinary{ 66 ID: "foo", 67 IDs: []string{"foo"}, 68 NameTemplate: "{{ .ProjectName }}", 69 }, ctx.Config.UniversalBinaries[0]) 70 }) 71 72 t.Run("given name", func(t *testing.T) { 73 ctx := testctx.NewWithCfg(config.Project{ 74 ProjectName: "proj", 75 UniversalBinaries: []config.UniversalBinary{ 76 {NameTemplate: "foo"}, 77 }, 78 }) 79 require.NoError(t, Pipe{}.Default(ctx)) 80 require.Equal(t, config.UniversalBinary{ 81 ID: "proj", 82 IDs: []string{"proj"}, 83 NameTemplate: "foo", 84 }, ctx.Config.UniversalBinaries[0]) 85 }) 86 87 t.Run("duplicated ids", func(t *testing.T) { 88 ctx := testctx.NewWithCfg(config.Project{ 89 ProjectName: "proj", 90 UniversalBinaries: []config.UniversalBinary{ 91 {ID: "foo"}, 92 {ID: "foo"}, 93 }, 94 }) 95 require.EqualError(t, Pipe{}.Default(ctx), `found 2 universal_binaries with the ID 'foo', please fix your config`) 96 }) 97 } 98 99 func TestSkip(t *testing.T) { 100 t.Run("skip", func(t *testing.T) { 101 require.True(t, Pipe{}.Skip(testctx.New())) 102 }) 103 104 t.Run("dont skip", func(t *testing.T) { 105 ctx := testctx.NewWithCfg(config.Project{ 106 UniversalBinaries: []config.UniversalBinary{{}}, 107 }) 108 require.False(t, Pipe{}.Skip(ctx)) 109 }) 110 } 111 112 func TestRun(t *testing.T) { 113 dist := t.TempDir() 114 115 src := filepath.Join("testdata", "fake", "main.go") 116 paths := map[string]string{ 117 "amd64": filepath.Join(dist, "fake_darwin_amd64/fake"), 118 "arm64": filepath.Join(dist, "fake_darwin_arm64/fake"), 119 } 120 121 pre := filepath.Join(dist, "pre") 122 post := filepath.Join(dist, "post") 123 cfg := config.Project{ 124 Dist: dist, 125 UniversalBinaries: []config.UniversalBinary{ 126 { 127 ID: "foo", 128 IDs: []string{"foo"}, 129 NameTemplate: "foo", 130 Replace: true, 131 }, 132 }, 133 } 134 ctx1 := testctx.NewWithCfg(cfg) 135 136 ctx2 := testctx.NewWithCfg(config.Project{ 137 Dist: dist, 138 UniversalBinaries: []config.UniversalBinary{ 139 { 140 ID: "foo", 141 IDs: []string{"foo"}, 142 NameTemplate: "foo", 143 }, 144 }, 145 }) 146 147 ctx3 := testctx.NewWithCfg(config.Project{ 148 Dist: dist, 149 UniversalBinaries: []config.UniversalBinary{ 150 { 151 ID: "notfoo", 152 IDs: []string{"notfoo", "notbar"}, 153 NameTemplate: "notfoo", 154 }, 155 }, 156 }) 157 158 ctx4 := testctx.NewWithCfg(config.Project{ 159 Dist: dist, 160 UniversalBinaries: []config.UniversalBinary{ 161 { 162 ID: "foo", 163 IDs: []string{"foo"}, 164 NameTemplate: "foo", 165 }, 166 }, 167 }) 168 169 ctx5 := testctx.NewWithCfg(config.Project{ 170 Dist: dist, 171 UniversalBinaries: []config.UniversalBinary{ 172 { 173 ID: "foo", 174 IDs: []string{"foo"}, 175 NameTemplate: "foo", 176 Hooks: config.BuildHookConfig{ 177 Pre: []config.Hook{ 178 {Cmd: "touch " + pre}, 179 }, 180 Post: []config.Hook{ 181 {Cmd: "touch " + post}, 182 {Cmd: `sh -c 'echo "{{ .Name }} {{ .Os }} {{ .Arch }} {{ .Arm }} {{ .Target }} {{ .Ext }}" > {{ .Path }}.post'`, Output: true}, 183 }, 184 }, 185 }, 186 }, 187 }) 188 189 ctx6 := testctx.NewWithCfg(config.Project{ 190 Dist: dist, 191 UniversalBinaries: []config.UniversalBinary{ 192 { 193 ID: "foobar", 194 IDs: []string{"foo"}, 195 NameTemplate: "foo", 196 }, 197 }, 198 }) 199 200 modTime := time.Now().AddDate(-1, 0, 0).Round(1 * time.Second).UTC() 201 ctx7 := testctx.NewWithCfg(config.Project{ 202 Dist: dist, 203 UniversalBinaries: []config.UniversalBinary{ 204 { 205 ID: "foo", 206 IDs: []string{"foo"}, 207 NameTemplate: "foo", 208 ModTimestamp: fmt.Sprintf("%d", modTime.Unix()), 209 Hooks: config.BuildHookConfig{ 210 Pre: []config.Hook{ 211 {Cmd: "touch " + pre}, 212 }, 213 Post: []config.Hook{ 214 {Cmd: "touch " + post}, 215 {Cmd: `sh -c 'echo "{{ .Name }} {{ .Os }} {{ .Arch }} {{ .Arm }} {{ .Target }} {{ .Ext }}" > {{ .Path }}.post'`, Output: true}, 216 }, 217 }, 218 }, 219 }, 220 }) 221 222 for arch, path := range paths { 223 cmd := exec.Command("go", "build", "-o", path, src) 224 cmd.Env = append(os.Environ(), "GOOS=darwin", "GOARCH="+arch) 225 _, err := cmd.CombinedOutput() 226 require.NoError(t, err) 227 228 modTime := time.Unix(0, 0) 229 require.NoError(t, os.Chtimes(path, modTime, modTime)) 230 231 art := artifact.Artifact{ 232 Name: "fake", 233 Path: path, 234 Goos: "darwin", 235 Goarch: arch, 236 Type: artifact.Binary, 237 Extra: map[string]interface{}{ 238 artifact.ExtraBinary: "fake", 239 artifact.ExtraID: "foo", 240 }, 241 } 242 ctx1.Artifacts.Add(&art) 243 ctx2.Artifacts.Add(&art) 244 ctx5.Artifacts.Add(&art) 245 ctx6.Artifacts.Add(&art) 246 ctx7.Artifacts.Add(&art) 247 ctx4.Artifacts.Add(&artifact.Artifact{ 248 Name: "fake", 249 Path: path + "wrong", 250 Goos: "darwin", 251 Goarch: arch, 252 Type: artifact.Binary, 253 Extra: map[string]interface{}{ 254 artifact.ExtraBinary: "fake", 255 artifact.ExtraID: "foo", 256 }, 257 }) 258 } 259 260 t.Run("ensure new artifact id", func(t *testing.T) { 261 require.NoError(t, Pipe{}.Run(ctx6)) 262 unis := ctx6.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List() 263 require.Len(t, unis, 1) 264 checkUniversalBinary(t, unis[0]) 265 require.Equal(t, "foobar", unis[0].ID()) 266 }) 267 268 t.Run("replacing", func(t *testing.T) { 269 require.NoError(t, Pipe{}.Run(ctx1)) 270 require.Empty(t, ctx1.Artifacts.Filter(artifact.ByType(artifact.Binary)).List()) 271 unis := ctx1.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List() 272 require.Len(t, unis, 1) 273 checkUniversalBinary(t, unis[0]) 274 require.True(t, artifact.ExtraOr(*unis[0], artifact.ExtraReplaces, false)) 275 }) 276 277 t.Run("keeping", func(t *testing.T) { 278 require.NoError(t, Pipe{}.Run(ctx2)) 279 require.Len(t, ctx2.Artifacts.Filter(artifact.ByType(artifact.Binary)).List(), 2) 280 unis := ctx2.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List() 281 require.Len(t, unis, 1) 282 checkUniversalBinary(t, unis[0]) 283 require.False(t, artifact.ExtraOr(*unis[0], artifact.ExtraReplaces, true)) 284 }) 285 286 t.Run("bad template", func(t *testing.T) { 287 testlib.RequireTemplateError(t, Pipe{}.Run(testctx.NewWithCfg(config.Project{ 288 UniversalBinaries: []config.UniversalBinary{ 289 { 290 NameTemplate: "{{.Name}", 291 }, 292 }, 293 }))) 294 }) 295 296 t.Run("no darwin builds", func(t *testing.T) { 297 require.EqualError(t, Pipe{}.Run(ctx3), `no darwin binaries found with ids: notfoo, notbar`) 298 }) 299 300 t.Run("fail to open", func(t *testing.T) { 301 require.ErrorIs(t, Pipe{}.Run(ctx4), os.ErrNotExist) 302 }) 303 304 t.Run("hooks", func(t *testing.T) { 305 require.NoError(t, Pipe{}.Run(ctx5)) 306 require.FileExists(t, pre) 307 require.FileExists(t, post) 308 post := filepath.Join(dist, "foo_darwin_all/foo.post") 309 require.FileExists(t, post) 310 bts, err := os.ReadFile(post) 311 require.NoError(t, err) 312 require.Equal(t, "foo darwin all darwin_all \n", string(bts)) 313 }) 314 315 t.Run("failing pre-hook", func(t *testing.T) { 316 ctx := ctx5 317 ctx.Config.UniversalBinaries[0].Hooks.Pre = []config.Hook{{Cmd: "exit 1"}} 318 ctx.Config.UniversalBinaries[0].Hooks.Post = []config.Hook{{Cmd: "echo post"}} 319 err := Pipe{}.Run(ctx) 320 require.ErrorIs(t, err, exec.ErrNotFound) 321 require.ErrorContains(t, err, "pre hook failed") 322 }) 323 324 t.Run("failing post-hook", func(t *testing.T) { 325 ctx := ctx5 326 ctx.Config.UniversalBinaries[0].Hooks.Pre = []config.Hook{{Cmd: "echo pre"}} 327 ctx.Config.UniversalBinaries[0].Hooks.Post = []config.Hook{{Cmd: "exit 1"}} 328 err := Pipe{}.Run(ctx) 329 require.ErrorIs(t, err, exec.ErrNotFound) 330 require.ErrorContains(t, err, "post hook failed") 331 }) 332 333 t.Run("skipping post-hook", func(t *testing.T) { 334 ctx := ctx5 335 skips.Set(ctx, skips.PostBuildHooks) 336 ctx.Config.UniversalBinaries[0].Hooks.Post = []config.Hook{{Cmd: "exit 1"}} 337 require.NoError(t, Pipe{}.Run(ctx)) 338 }) 339 340 t.Run("skipping pre-hook", func(t *testing.T) { 341 ctx := ctx5 342 skips.Set(ctx, skips.PreBuildHooks) 343 ctx.Config.UniversalBinaries[0].Hooks.Pre = []config.Hook{{Cmd: "exit 1"}} 344 require.NoError(t, Pipe{}.Run(ctx)) 345 }) 346 347 t.Run("hook with env tmpl", func(t *testing.T) { 348 ctx := ctx5 349 ctx.Skips[string(skips.PostBuildHooks)] = false 350 ctx.Skips[string(skips.PreBuildHooks)] = false 351 ctx.Config.UniversalBinaries[0].Hooks.Pre = []config.Hook{{ 352 Cmd: "echo {{.Env.FOO}}", 353 Env: []string{"FOO=foo-{{.Tag}}"}, 354 }} 355 ctx.Config.UniversalBinaries[0].Hooks.Post = []config.Hook{} 356 require.NoError(t, Pipe{}.Run(ctx)) 357 }) 358 359 t.Run("hook with bad env tmpl", func(t *testing.T) { 360 ctx := ctx5 361 ctx.Skips[string(skips.PostBuildHooks)] = false 362 ctx.Skips[string(skips.PreBuildHooks)] = false 363 ctx.Config.UniversalBinaries[0].Hooks.Pre = []config.Hook{{ 364 Cmd: "echo blah", 365 Env: []string{"FOO=foo-{{.Tag}"}, 366 }} 367 ctx.Config.UniversalBinaries[0].Hooks.Post = []config.Hook{} 368 testlib.RequireTemplateError(t, Pipe{}.Run(ctx)) 369 }) 370 371 t.Run("hook with bad dir tmpl", func(t *testing.T) { 372 ctx := ctx5 373 ctx.Config.UniversalBinaries[0].Hooks.Pre = []config.Hook{{ 374 Cmd: "echo blah", 375 Dir: "{{.Tag}", 376 }} 377 ctx.Config.UniversalBinaries[0].Hooks.Post = []config.Hook{} 378 testlib.RequireTemplateError(t, Pipe{}.Run(ctx)) 379 }) 380 381 t.Run("hook with bad cmd tmpl", func(t *testing.T) { 382 ctx := ctx5 383 ctx.Config.UniversalBinaries[0].Hooks.Pre = []config.Hook{{ 384 Cmd: "echo blah-{{.Tag }", 385 }} 386 ctx.Config.UniversalBinaries[0].Hooks.Post = []config.Hook{} 387 testlib.RequireTemplateError(t, Pipe{}.Run(ctx)) 388 }) 389 390 t.Run("mod timestamp", func(t *testing.T) { 391 ctx := ctx7 392 require.NoError(t, Pipe{}.Run(ctx)) 393 unibins := ctx.Artifacts.Filter(artifact.ByType(artifact.UniversalBinary)).List() 394 require.Len(t, unibins, 1) 395 stat, err := os.Stat(unibins[0].Path) 396 require.NoError(t, err) 397 require.Equal(t, modTime.Unix(), stat.ModTime().Unix()) 398 }) 399 400 t.Run("bad mod timestamp", func(t *testing.T) { 401 ctx := ctx5 402 ctx.Config.UniversalBinaries[0].ModTimestamp = "not a number" 403 ctx.Config.UniversalBinaries[0].Hooks.Pre = []config.Hook{} 404 ctx.Config.UniversalBinaries[0].Hooks.Post = []config.Hook{} 405 require.ErrorIs(t, Pipe{}.Run(ctx), strconv.ErrSyntax) 406 }) 407 } 408 409 func checkUniversalBinary(tb testing.TB, unibin *artifact.Artifact) { 410 tb.Helper() 411 412 require.True(tb, strings.HasSuffix(unibin.Path, unibin.ID()+"_darwin_all/foo")) 413 f, err := macho.OpenFat(unibin.Path) 414 require.NoError(tb, err) 415 require.Len(tb, f.Arches, 2) 416 }