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