github.com/goreleaser/goreleaser@v1.25.1/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/goreleaser/goreleaser/internal/archivefiles"
    17  	"github.com/goreleaser/goreleaser/internal/artifact"
    18  	"github.com/goreleaser/goreleaser/internal/deprecate"
    19  	"github.com/goreleaser/goreleaser/internal/ids"
    20  	"github.com/goreleaser/goreleaser/internal/semerrgroup"
    21  	"github.com/goreleaser/goreleaser/internal/tmpl"
    22  	"github.com/goreleaser/goreleaser/pkg/archive"
    23  	"github.com/goreleaser/goreleaser/pkg/config"
    24  	"github.com/goreleaser/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.StripParentBinaryFolder {
    63  			archive.StripBinaryDirectory = true
    64  			deprecate.Notice(ctx, "archives.strip_parent_binary_folder")
    65  		}
    66  		if archive.RLCP != "" && archive.Format != "binary" && len(archive.Files) > 0 {
    67  			deprecate.Notice(ctx, "archives.rlcp")
    68  		}
    69  		if len(archive.Files) == 0 {
    70  			archive.Files = []config.File{
    71  				{Source: "license*", Default: true},
    72  				{Source: "LICENSE*", Default: true},
    73  				{Source: "readme*", Default: true},
    74  				{Source: "README*", Default: true},
    75  				{Source: "changelog*", Default: true},
    76  				{Source: "CHANGELOG*", Default: true},
    77  			}
    78  		}
    79  		if archive.NameTemplate == "" {
    80  			archive.NameTemplate = defaultNameTemplate
    81  			if archive.Format == "binary" {
    82  				archive.NameTemplate = defaultBinaryNameTemplate
    83  			}
    84  		}
    85  		ids.Inc(archive.ID)
    86  	}
    87  	return ids.Validate()
    88  }
    89  
    90  // Run the pipe.
    91  func (Pipe) Run(ctx *context.Context) error {
    92  	g := semerrgroup.New(ctx.Parallelism)
    93  	for i, archive := range ctx.Config.Archives {
    94  		archive := archive
    95  		if archive.Meta {
    96  			g.Go(func() error {
    97  				return createMeta(ctx, archive)
    98  			})
    99  			continue
   100  		}
   101  
   102  		filter := []artifact.Filter{artifact.Or(
   103  			artifact.ByType(artifact.Binary),
   104  			artifact.ByType(artifact.UniversalBinary),
   105  			artifact.ByType(artifact.Header),
   106  			artifact.ByType(artifact.CArchive),
   107  			artifact.ByType(artifact.CShared),
   108  		)}
   109  		if len(archive.Builds) > 0 {
   110  			filter = append(filter, artifact.ByIDs(archive.Builds...))
   111  		}
   112  		artifacts := ctx.Artifacts.Filter(artifact.And(filter...)).GroupByPlatform()
   113  		if err := checkArtifacts(artifacts); err != nil && archive.Format != "binary" && !archive.AllowDifferentBinaryCount {
   114  			return fmt.Errorf("invalid archive: %d: %w", i, ErrArchiveDifferentBinaryCount)
   115  		}
   116  		for group, artifacts := range artifacts {
   117  			log.Debugf("group %s has %d binaries", group, len(artifacts))
   118  			artifacts := artifacts
   119  			format := packageFormat(archive, artifacts[0].Goos)
   120  			switch format {
   121  			case "none":
   122  				// do nothing
   123  				log.WithField("goos", artifacts[0].Goos).Info("ignored due to format override to 'none'")
   124  			case "binary":
   125  				g.Go(func() error {
   126  					return skip(ctx, archive, artifacts)
   127  				})
   128  			default:
   129  				g.Go(func() error {
   130  					return create(ctx, archive, artifacts, format)
   131  				})
   132  			}
   133  		}
   134  	}
   135  	return g.Wait()
   136  }
   137  
   138  func checkArtifacts(artifacts map[string][]*artifact.Artifact) error {
   139  	lens := map[int]bool{}
   140  	for _, v := range artifacts {
   141  		lens[len(v)] = true
   142  	}
   143  	if len(lens) <= 1 {
   144  		return nil
   145  	}
   146  	return ErrArchiveDifferentBinaryCount
   147  }
   148  
   149  func createMeta(ctx *context.Context, arch config.Archive) error {
   150  	return create(ctx, arch, nil, arch.Format)
   151  }
   152  
   153  func create(ctx *context.Context, arch config.Archive, binaries []*artifact.Artifact, format string) error {
   154  	template := tmpl.New(ctx)
   155  	if len(binaries) > 0 {
   156  		template = template.WithArtifact(binaries[0])
   157  	}
   158  	folder, err := template.Apply(arch.NameTemplate)
   159  	if err != nil {
   160  		return err
   161  	}
   162  	archivePath := filepath.Join(ctx.Config.Dist, folder+"."+format)
   163  	lock.Lock()
   164  	if err := os.MkdirAll(filepath.Dir(archivePath), 0o755|os.ModeDir); err != nil {
   165  		lock.Unlock()
   166  		return err
   167  	}
   168  	if _, err = os.Stat(archivePath); !errors.Is(err, fs.ErrNotExist) {
   169  		lock.Unlock()
   170  		return fmt.Errorf("archive named %s already exists. Check your archive name template", archivePath)
   171  	}
   172  	archiveFile, err := os.Create(archivePath)
   173  	if err != nil {
   174  		lock.Unlock()
   175  		return fmt.Errorf("failed to create directory %s: %w", archivePath, err)
   176  	}
   177  	lock.Unlock()
   178  	defer archiveFile.Close()
   179  
   180  	log := log.WithField("archive", archivePath)
   181  	log.Info("creating")
   182  
   183  	wrap, err := template.Apply(wrapFolder(arch))
   184  	if err != nil {
   185  		return err
   186  	}
   187  	a, err := archive.New(archiveFile, format)
   188  	if err != nil {
   189  		return err
   190  	}
   191  	a = NewEnhancedArchive(a, wrap)
   192  	defer a.Close()
   193  
   194  	files, err := archivefiles.Eval(template, arch.Files)
   195  	if err != nil {
   196  		return fmt.Errorf("failed to find files to archive: %w", err)
   197  	}
   198  	if arch.Meta && len(files) == 0 {
   199  		return fmt.Errorf("no files found")
   200  	}
   201  	for _, f := range files {
   202  		if err = a.Add(f); err != nil {
   203  			return fmt.Errorf("failed to add: '%s' -> '%s': %w", f.Source, f.Destination, err)
   204  		}
   205  	}
   206  	bins := []string{}
   207  	for _, binary := range binaries {
   208  		dst := binary.Name
   209  		if arch.StripBinaryDirectory {
   210  			dst = filepath.Base(dst)
   211  		}
   212  		if err := a.Add(config.File{
   213  			Source:      binary.Path,
   214  			Destination: dst,
   215  			Info:        arch.BuildsInfo,
   216  		}); err != nil {
   217  			return fmt.Errorf("failed to add: '%s' -> '%s': %w", binary.Path, dst, err)
   218  		}
   219  		bins = append(bins, binary.Name)
   220  	}
   221  	art := &artifact.Artifact{
   222  		Type: artifact.UploadableArchive,
   223  		Name: folder + "." + format,
   224  		Path: archivePath,
   225  		Extra: map[string]interface{}{
   226  			artifact.ExtraID:        arch.ID,
   227  			artifact.ExtraFormat:    format,
   228  			artifact.ExtraWrappedIn: wrap,
   229  			artifact.ExtraBinaries:  bins,
   230  		},
   231  	}
   232  	if len(binaries) > 0 {
   233  		art.Goos = binaries[0].Goos
   234  		art.Goarch = binaries[0].Goarch
   235  		art.Goarm = binaries[0].Goarm
   236  		art.Gomips = binaries[0].Gomips
   237  		art.Goamd64 = binaries[0].Goamd64
   238  		art.Extra[artifact.ExtraReplaces] = binaries[0].Extra[artifact.ExtraReplaces]
   239  	}
   240  
   241  	ctx.Artifacts.Add(art)
   242  	return nil
   243  }
   244  
   245  func wrapFolder(a config.Archive) string {
   246  	switch a.WrapInDirectory {
   247  	case "true":
   248  		return a.NameTemplate
   249  	case "false":
   250  		return ""
   251  	default:
   252  		return a.WrapInDirectory
   253  	}
   254  }
   255  
   256  func skip(ctx *context.Context, archive config.Archive, binaries []*artifact.Artifact) error {
   257  	for _, binary := range binaries {
   258  		name, err := tmpl.New(ctx).WithArtifact(binary).Apply(archive.NameTemplate)
   259  		if err != nil {
   260  			return err
   261  		}
   262  		finalName := name + artifact.ExtraOr(*binary, artifact.ExtraExt, "")
   263  		log.WithField("binary", binary.Name).
   264  			WithField("name", finalName).
   265  			Info("skip archiving")
   266  		ctx.Artifacts.Add(&artifact.Artifact{
   267  			Type:    artifact.UploadableBinary,
   268  			Name:    finalName,
   269  			Path:    binary.Path,
   270  			Goos:    binary.Goos,
   271  			Goarch:  binary.Goarch,
   272  			Goarm:   binary.Goarm,
   273  			Gomips:  binary.Gomips,
   274  			Goamd64: binary.Goamd64,
   275  			Extra: map[string]interface{}{
   276  				artifact.ExtraID:       archive.ID,
   277  				artifact.ExtraFormat:   archive.Format,
   278  				artifact.ExtraBinary:   binary.Name,
   279  				artifact.ExtraReplaces: binaries[0].Extra[artifact.ExtraReplaces],
   280  			},
   281  		})
   282  	}
   283  	return nil
   284  }
   285  
   286  func packageFormat(archive config.Archive, platform string) string {
   287  	for _, override := range archive.FormatOverrides {
   288  		if strings.HasPrefix(platform, override.Goos) {
   289  			return override.Format
   290  		}
   291  	}
   292  	return archive.Format
   293  }
   294  
   295  // NewEnhancedArchive enhances a pre-existing archive.Archive instance
   296  // with this pipe specifics.
   297  func NewEnhancedArchive(a archive.Archive, wrap string) archive.Archive {
   298  	return EnhancedArchive{
   299  		a:     a,
   300  		wrap:  wrap,
   301  		files: map[string]string{},
   302  	}
   303  }
   304  
   305  // EnhancedArchive is an archive.Archive implementation which decorates an
   306  // archive.Archive adding wrap directory support, logging and windows
   307  // backslash fixes.
   308  type EnhancedArchive struct {
   309  	a     archive.Archive
   310  	wrap  string
   311  	files map[string]string
   312  }
   313  
   314  // Add adds a file.
   315  func (d EnhancedArchive) Add(f config.File) error {
   316  	name := strings.ReplaceAll(filepath.Join(d.wrap, f.Destination), "\\", "/")
   317  	log.Debugf("adding file: %s as %s", f.Source, 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  }