github.com/windmeup/goreleaser@v1.21.95/internal/pipe/archive/archive.go (about)

     1  // Package archive implements the pipe interface with the intent of
     2  // archiving and compressing the binaries, readme, and other artifacts. It
     3  // also provides an Archive interface which represents an archiving format.
     4  package archive
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io/fs"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"sync"
    14  
    15  	"github.com/caarlos0/log"
    16  	"github.com/windmeup/goreleaser/internal/archivefiles"
    17  	"github.com/windmeup/goreleaser/internal/artifact"
    18  	"github.com/windmeup/goreleaser/internal/deprecate"
    19  	"github.com/windmeup/goreleaser/internal/ids"
    20  	"github.com/windmeup/goreleaser/internal/semerrgroup"
    21  	"github.com/windmeup/goreleaser/internal/tmpl"
    22  	"github.com/windmeup/goreleaser/pkg/archive"
    23  	"github.com/windmeup/goreleaser/pkg/config"
    24  	"github.com/windmeup/goreleaser/pkg/context"
    25  )
    26  
    27  const (
    28  	defaultNameTemplateSuffix = `{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}`
    29  	defaultNameTemplate       = "{{ .ProjectName }}_" + defaultNameTemplateSuffix
    30  	defaultBinaryNameTemplate = "{{ .Binary }}_" + defaultNameTemplateSuffix
    31  )
    32  
    33  // ErrArchiveDifferentBinaryCount happens when an archive uses several builds which have different goos/goarch/etc sets,
    34  // causing the archives for some platforms to have more binaries than others.
    35  // GoReleaser breaks in these cases as it will only cause confusion to other users.
    36  var ErrArchiveDifferentBinaryCount = errors.New("archive has different count of binaries for each platform, which may cause your users confusion.\nLearn more at https://goreleaser.com/errors/multiple-binaries-archive\n") // nolint:revive
    37  
    38  // nolint: gochecknoglobals
    39  var lock sync.Mutex
    40  
    41  // Pipe for archive.
    42  type Pipe struct{}
    43  
    44  func (Pipe) String() string {
    45  	return "archives"
    46  }
    47  
    48  // Default sets the pipe defaults.
    49  func (Pipe) Default(ctx *context.Context) error {
    50  	ids := ids.New("archives")
    51  	if len(ctx.Config.Archives) == 0 {
    52  		ctx.Config.Archives = append(ctx.Config.Archives, config.Archive{})
    53  	}
    54  	for i := range ctx.Config.Archives {
    55  		archive := &ctx.Config.Archives[i]
    56  		if archive.Format == "" {
    57  			archive.Format = "tar.gz"
    58  		}
    59  		if archive.ID == "" {
    60  			archive.ID = "default"
    61  		}
    62  		if archive.RLCP != "" && archive.Format != "binary" && len(archive.Files) > 0 {
    63  			deprecate.Notice(ctx, "archives.rlcp")
    64  		}
    65  		if len(archive.Files) == 0 {
    66  			archive.Files = []config.File{
    67  				{Source: "license*", Default: true},
    68  				{Source: "LICENSE*", Default: true},
    69  				{Source: "readme*", Default: true},
    70  				{Source: "README*", Default: true},
    71  				{Source: "changelog*", Default: true},
    72  				{Source: "CHANGELOG*", Default: true},
    73  			}
    74  		}
    75  		if archive.NameTemplate == "" {
    76  			archive.NameTemplate = defaultNameTemplate
    77  			if archive.Format == "binary" {
    78  				archive.NameTemplate = defaultBinaryNameTemplate
    79  			}
    80  		}
    81  		ids.Inc(archive.ID)
    82  	}
    83  	return ids.Validate()
    84  }
    85  
    86  // Run the pipe.
    87  func (Pipe) Run(ctx *context.Context) error {
    88  	g := semerrgroup.New(ctx.Parallelism)
    89  	for i, archive := range ctx.Config.Archives {
    90  		archive := archive
    91  		if archive.Meta {
    92  			g.Go(func() error {
    93  				return createMeta(ctx, archive)
    94  			})
    95  			continue
    96  		}
    97  
    98  		filter := []artifact.Filter{artifact.Or(
    99  			artifact.ByType(artifact.Binary),
   100  			artifact.ByType(artifact.UniversalBinary),
   101  			artifact.ByType(artifact.Header),
   102  			artifact.ByType(artifact.CArchive),
   103  			artifact.ByType(artifact.CShared),
   104  		)}
   105  		if len(archive.Builds) > 0 {
   106  			filter = append(filter, artifact.ByIDs(archive.Builds...))
   107  		}
   108  		artifacts := ctx.Artifacts.Filter(artifact.And(filter...)).GroupByPlatform()
   109  		if err := checkArtifacts(artifacts); err != nil && archive.Format != "binary" && !archive.AllowDifferentBinaryCount {
   110  			return fmt.Errorf("invalid archive: %d: %w", i, ErrArchiveDifferentBinaryCount)
   111  		}
   112  		for group, artifacts := range artifacts {
   113  			log.Debugf("group %s has %d binaries", group, len(artifacts))
   114  			artifacts := artifacts
   115  			if packageFormat(archive, artifacts[0].Goos) == "binary" {
   116  				g.Go(func() error {
   117  					return skip(ctx, archive, artifacts)
   118  				})
   119  				continue
   120  			}
   121  			g.Go(func() error {
   122  				return create(ctx, archive, artifacts)
   123  			})
   124  		}
   125  	}
   126  	return g.Wait()
   127  }
   128  
   129  func checkArtifacts(artifacts map[string][]*artifact.Artifact) error {
   130  	lens := map[int]bool{}
   131  	for _, v := range artifacts {
   132  		lens[len(v)] = true
   133  	}
   134  	if len(lens) <= 1 {
   135  		return nil
   136  	}
   137  	return ErrArchiveDifferentBinaryCount
   138  }
   139  
   140  func createMeta(ctx *context.Context, arch config.Archive) error {
   141  	return doCreate(ctx, arch, nil, arch.Format)
   142  }
   143  
   144  func create(ctx *context.Context, arch config.Archive, binaries []*artifact.Artifact) error {
   145  	format := packageFormat(arch, binaries[0].Goos)
   146  	return doCreate(ctx, arch, binaries, format)
   147  }
   148  
   149  func doCreate(ctx *context.Context, arch config.Archive, binaries []*artifact.Artifact, format string) error {
   150  	template := tmpl.New(ctx)
   151  	if len(binaries) > 0 {
   152  		template = template.WithArtifact(binaries[0])
   153  	}
   154  	folder, err := template.Apply(arch.NameTemplate)
   155  	if err != nil {
   156  		return err
   157  	}
   158  	archivePath := filepath.Join(ctx.Config.Dist, folder+"."+format)
   159  	lock.Lock()
   160  	if err := os.MkdirAll(filepath.Dir(archivePath), 0o755|os.ModeDir); err != nil {
   161  		lock.Unlock()
   162  		return err
   163  	}
   164  	if _, err = os.Stat(archivePath); !errors.Is(err, fs.ErrNotExist) {
   165  		lock.Unlock()
   166  		return fmt.Errorf("archive named %s already exists. Check your archive name template", archivePath)
   167  	}
   168  	archiveFile, err := os.Create(archivePath)
   169  	if err != nil {
   170  		lock.Unlock()
   171  		return fmt.Errorf("failed to create directory %s: %w", archivePath, err)
   172  	}
   173  	lock.Unlock()
   174  	defer archiveFile.Close()
   175  
   176  	log := log.WithField("archive", archivePath)
   177  	log.Info("creating")
   178  
   179  	wrap, err := template.Apply(wrapFolder(arch))
   180  	if err != nil {
   181  		return err
   182  	}
   183  	a, err := archive.New(archiveFile, format)
   184  	if err != nil {
   185  		return err
   186  	}
   187  	a = NewEnhancedArchive(a, wrap)
   188  	defer a.Close()
   189  
   190  	files, err := archivefiles.Eval(template, arch.Files)
   191  	if err != nil {
   192  		return fmt.Errorf("failed to find files to archive: %w", err)
   193  	}
   194  	if arch.Meta && len(files) == 0 {
   195  		return fmt.Errorf("no files found")
   196  	}
   197  	for _, f := range files {
   198  		if err = a.Add(f); err != nil {
   199  			return fmt.Errorf("failed to add: '%s' -> '%s': %w", f.Source, f.Destination, err)
   200  		}
   201  	}
   202  	bins := []string{}
   203  	for _, binary := range binaries {
   204  		dst := binary.Name
   205  		if arch.StripParentBinaryFolder {
   206  			dst = filepath.Base(dst)
   207  		}
   208  		if err := a.Add(config.File{
   209  			Source:      binary.Path,
   210  			Destination: dst,
   211  			Info:        arch.BuildsInfo,
   212  		}); err != nil {
   213  			return fmt.Errorf("failed to add: '%s' -> '%s': %w", binary.Path, dst, err)
   214  		}
   215  		bins = append(bins, binary.Name)
   216  	}
   217  	art := &artifact.Artifact{
   218  		Type: artifact.UploadableArchive,
   219  		Name: folder + "." + format,
   220  		Path: archivePath,
   221  		Extra: map[string]interface{}{
   222  			artifact.ExtraID:        arch.ID,
   223  			artifact.ExtraFormat:    format,
   224  			artifact.ExtraWrappedIn: wrap,
   225  			artifact.ExtraBinaries:  bins,
   226  		},
   227  	}
   228  	if len(binaries) > 0 {
   229  		art.Goos = binaries[0].Goos
   230  		art.Goarch = binaries[0].Goarch
   231  		art.Goarm = binaries[0].Goarm
   232  		art.Gomips = binaries[0].Gomips
   233  		art.Goamd64 = binaries[0].Goamd64
   234  		art.Extra[artifact.ExtraReplaces] = binaries[0].Extra[artifact.ExtraReplaces]
   235  	}
   236  
   237  	ctx.Artifacts.Add(art)
   238  	return nil
   239  }
   240  
   241  func wrapFolder(a config.Archive) string {
   242  	switch a.WrapInDirectory {
   243  	case "true":
   244  		return a.NameTemplate
   245  	case "false":
   246  		return ""
   247  	default:
   248  		return a.WrapInDirectory
   249  	}
   250  }
   251  
   252  func skip(ctx *context.Context, archive config.Archive, binaries []*artifact.Artifact) error {
   253  	for _, binary := range binaries {
   254  		name, err := tmpl.New(ctx).WithArtifact(binary).Apply(archive.NameTemplate)
   255  		if err != nil {
   256  			return err
   257  		}
   258  		finalName := name + artifact.ExtraOr(*binary, artifact.ExtraExt, "")
   259  		log.WithField("binary", binary.Name).
   260  			WithField("name", finalName).
   261  			Info("skip archiving")
   262  		ctx.Artifacts.Add(&artifact.Artifact{
   263  			Type:    artifact.UploadableBinary,
   264  			Name:    finalName,
   265  			Path:    binary.Path,
   266  			Goos:    binary.Goos,
   267  			Goarch:  binary.Goarch,
   268  			Goarm:   binary.Goarm,
   269  			Gomips:  binary.Gomips,
   270  			Goamd64: binary.Goamd64,
   271  			Extra: map[string]interface{}{
   272  				artifact.ExtraID:       archive.ID,
   273  				artifact.ExtraFormat:   archive.Format,
   274  				artifact.ExtraBinary:   binary.Name,
   275  				artifact.ExtraReplaces: binaries[0].Extra[artifact.ExtraReplaces],
   276  			},
   277  		})
   278  	}
   279  	return nil
   280  }
   281  
   282  func packageFormat(archive config.Archive, platform string) string {
   283  	for _, override := range archive.FormatOverrides {
   284  		if strings.HasPrefix(platform, override.Goos) {
   285  			return override.Format
   286  		}
   287  	}
   288  	return archive.Format
   289  }
   290  
   291  // NewEnhancedArchive enhances a pre-existing archive.Archive instance
   292  // with this pipe specifics.
   293  func NewEnhancedArchive(a archive.Archive, wrap string) archive.Archive {
   294  	return EnhancedArchive{
   295  		a:     a,
   296  		wrap:  wrap,
   297  		files: map[string]string{},
   298  	}
   299  }
   300  
   301  // EnhancedArchive is an archive.Archive implementation which decorates an
   302  // archive.Archive adding wrap directory support, logging and windows
   303  // backslash fixes.
   304  type EnhancedArchive struct {
   305  	a     archive.Archive
   306  	wrap  string
   307  	files map[string]string
   308  }
   309  
   310  // Add adds a file.
   311  func (d EnhancedArchive) Add(f config.File) error {
   312  	name := strings.ReplaceAll(filepath.Join(d.wrap, f.Destination), "\\", "/")
   313  	log.Debugf("adding file: %s as %s", f.Source, name)
   314  	ff := config.File{
   315  		Source:      f.Source,
   316  		Destination: name,
   317  		Info:        f.Info,
   318  	}
   319  	return d.a.Add(ff)
   320  }
   321  
   322  // Close closes the underlying archive.
   323  func (d EnhancedArchive) Close() error {
   324  	return d.a.Close()
   325  }