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  }