github.com/amane3/goreleaser@v0.182.0/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  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/apex/log"
    15  	"github.com/campoy/unique"
    16  	"github.com/goreleaser/fileglob"
    17  
    18  	"github.com/amane3/goreleaser/internal/artifact"
    19  	"github.com/amane3/goreleaser/internal/ids"
    20  	"github.com/amane3/goreleaser/internal/semerrgroup"
    21  	"github.com/amane3/goreleaser/internal/tmpl"
    22  	"github.com/amane3/goreleaser/pkg/archive"
    23  	"github.com/amane3/goreleaser/pkg/config"
    24  	"github.com/amane3/goreleaser/pkg/context"
    25  )
    26  
    27  const (
    28  	defaultNameTemplate       = "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
    29  	defaultBinaryNameTemplate = "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
    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 built binaries for each platform, which may cause your users confusion. Please make sure all builds used have the same set of goos/goarch/etc or split it into multiple archives")
    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  	var 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  		var 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 = []string{
    63  				"licence*",
    64  				"LICENCE*",
    65  				"license*",
    66  				"LICENSE*",
    67  				"readme*",
    68  				"README*",
    69  				"changelog*",
    70  				"CHANGELOG*",
    71  			}
    72  		}
    73  		if archive.NameTemplate == "" {
    74  			archive.NameTemplate = defaultNameTemplate
    75  			if archive.Format == "binary" {
    76  				archive.NameTemplate = defaultBinaryNameTemplate
    77  			}
    78  		}
    79  		if len(archive.Builds) == 0 {
    80  			for _, build := range ctx.Config.Builds {
    81  				archive.Builds = append(archive.Builds, build.ID)
    82  			}
    83  		}
    84  		ids.Inc(archive.ID)
    85  	}
    86  	return ids.Validate()
    87  }
    88  
    89  // Run the pipe.
    90  func (Pipe) Run(ctx *context.Context) error {
    91  	var g = semerrgroup.New(ctx.Parallelism)
    92  	for i, archive := range ctx.Config.Archives {
    93  		archive := archive
    94  		var artifacts = ctx.Artifacts.Filter(
    95  			artifact.And(
    96  				artifact.ByType(artifact.Binary),
    97  				artifact.ByIDs(archive.Builds...),
    98  			),
    99  		).GroupByPlatform()
   100  		if err := checkArtifacts(artifacts); err != nil && !archive.AllowDifferentBinaryCount {
   101  			return fmt.Errorf("invalid archive: %d: %w", i, ErrArchiveDifferentBinaryCount)
   102  		}
   103  		for group, artifacts := range artifacts {
   104  			log.Debugf("group %s has %d binaries", group, len(artifacts))
   105  			artifacts := artifacts
   106  			g.Go(func() error {
   107  				if packageFormat(archive, artifacts[0].Goos) == "binary" {
   108  					return skip(ctx, archive, artifacts)
   109  				}
   110  				return create(ctx, archive, artifacts)
   111  			})
   112  		}
   113  	}
   114  	return g.Wait()
   115  }
   116  
   117  func checkArtifacts(artifacts map[string][]*artifact.Artifact) error {
   118  	var lens = map[int]bool{}
   119  	for _, v := range artifacts {
   120  		lens[len(v)] = true
   121  	}
   122  	if len(lens) <= 1 {
   123  		return nil
   124  	}
   125  	return ErrArchiveDifferentBinaryCount
   126  }
   127  
   128  func create(ctx *context.Context, arch config.Archive, binaries []*artifact.Artifact) error {
   129  	var format = packageFormat(arch, binaries[0].Goos)
   130  	folder, err := tmpl.New(ctx).
   131  		WithArtifact(binaries[0], arch.Replacements).
   132  		Apply(arch.NameTemplate)
   133  	if err != nil {
   134  		return err
   135  	}
   136  	archivePath := filepath.Join(ctx.Config.Dist, folder+"."+format)
   137  	lock.Lock()
   138  	if err := os.MkdirAll(filepath.Dir(archivePath), 0755|os.ModeDir); err != nil {
   139  		lock.Unlock()
   140  		return err
   141  	}
   142  	if _, err = os.Stat(archivePath); !os.IsNotExist(err) {
   143  		lock.Unlock()
   144  		return fmt.Errorf("archive named %s already exists. Check your archive name template", archivePath)
   145  	}
   146  	archiveFile, err := os.Create(archivePath)
   147  	if err != nil {
   148  		lock.Unlock()
   149  		return fmt.Errorf("failed to create directory %s: %w", archivePath, err)
   150  	}
   151  	lock.Unlock()
   152  	defer archiveFile.Close()
   153  
   154  	var log = log.WithField("archive", archivePath)
   155  	log.Info("creating")
   156  
   157  	template := tmpl.New(ctx).
   158  		WithArtifact(binaries[0], arch.Replacements)
   159  	wrap, err := template.Apply(wrapFolder(arch))
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	var a = NewEnhancedArchive(archive.New(archiveFile), wrap)
   165  	defer a.Close()
   166  
   167  	files, err := findFiles(template, arch)
   168  	if err != nil {
   169  		return fmt.Errorf("failed to find files to archive: %w", err)
   170  	}
   171  	for _, f := range files {
   172  		if err = a.Add(f, f); err != nil {
   173  			return fmt.Errorf("failed to add %s to the archive: %w", f, err)
   174  		}
   175  	}
   176  	for _, binary := range binaries {
   177  		if err := a.Add(binary.Name, binary.Path); err != nil {
   178  			return fmt.Errorf("failed to add %s -> %s to the archive: %w", binary.Path, binary.Name, err)
   179  		}
   180  	}
   181  	ctx.Artifacts.Add(&artifact.Artifact{
   182  		Type:   artifact.UploadableArchive,
   183  		Name:   folder + "." + format,
   184  		Path:   archivePath,
   185  		Goos:   binaries[0].Goos,
   186  		Goarch: binaries[0].Goarch,
   187  		Goarm:  binaries[0].Goarm,
   188  		Gomips: binaries[0].Gomips,
   189  		Extra: map[string]interface{}{
   190  			"Builds":    binaries,
   191  			"ID":        arch.ID,
   192  			"Format":    arch.Format,
   193  			"WrappedIn": wrap,
   194  		},
   195  	})
   196  	return nil
   197  }
   198  
   199  func wrapFolder(a config.Archive) string {
   200  	switch a.WrapInDirectory {
   201  	case "true":
   202  		return a.NameTemplate
   203  	case "false":
   204  		return ""
   205  	default:
   206  		return a.WrapInDirectory
   207  	}
   208  }
   209  
   210  func skip(ctx *context.Context, archive config.Archive, binaries []*artifact.Artifact) error {
   211  	for _, binary := range binaries {
   212  		log.WithField("binary", binary.Name).Info("skip archiving")
   213  		name, err := tmpl.New(ctx).
   214  			WithArtifact(binary, archive.Replacements).
   215  			Apply(archive.NameTemplate)
   216  		if err != nil {
   217  			return err
   218  		}
   219  		ctx.Artifacts.Add(&artifact.Artifact{
   220  			Type:   artifact.UploadableBinary,
   221  			Name:   name + binary.ExtraOr("Ext", "").(string),
   222  			Path:   binary.Path,
   223  			Goos:   binary.Goos,
   224  			Goarch: binary.Goarch,
   225  			Goarm:  binary.Goarm,
   226  			Gomips: binary.Gomips,
   227  			Extra: map[string]interface{}{
   228  				"Builds": []*artifact.Artifact{binary},
   229  				"ID":     archive.ID,
   230  				"Format": archive.Format,
   231  			},
   232  		})
   233  	}
   234  	return nil
   235  }
   236  
   237  func findFiles(template *tmpl.Template, archive config.Archive) (result []string, err error) {
   238  	for _, glob := range archive.Files {
   239  		replaced, err := template.Apply(glob)
   240  		if err != nil {
   241  			return result, fmt.Errorf("failed to apply template %s: %w", glob, err)
   242  		}
   243  		files, err := fileglob.Glob(replaced)
   244  		if err != nil {
   245  			return result, fmt.Errorf("globbing failed for pattern %s: %w", glob, err)
   246  		}
   247  		result = append(result, files...)
   248  	}
   249  	// remove duplicates
   250  	unique.Slice(&result, func(i, j int) bool {
   251  		return strings.Compare(result[i], result[j]) < 0
   252  	})
   253  	return
   254  }
   255  
   256  func packageFormat(archive config.Archive, platform string) string {
   257  	for _, override := range archive.FormatOverrides {
   258  		if strings.HasPrefix(platform, override.Goos) {
   259  			return override.Format
   260  		}
   261  	}
   262  	return archive.Format
   263  }
   264  
   265  // NewEnhancedArchive enhances a pre-existing archive.Archive instance
   266  // with this pipe specifics.
   267  func NewEnhancedArchive(a archive.Archive, wrap string) archive.Archive {
   268  	return EnhancedArchive{
   269  		a:     a,
   270  		wrap:  wrap,
   271  		files: map[string]string{},
   272  	}
   273  }
   274  
   275  // EnhancedArchive is an archive.Archive implementation which decorates an
   276  // archive.Archive adding wrap directory support, logging and windows
   277  // backslash fixes.
   278  type EnhancedArchive struct {
   279  	a     archive.Archive
   280  	wrap  string
   281  	files map[string]string
   282  }
   283  
   284  // Add adds a file.
   285  func (d EnhancedArchive) Add(name, path string) error {
   286  	name = strings.ReplaceAll(filepath.Join(d.wrap, name), "\\", "/")
   287  	log.Debugf("adding file: %s as %s", path, name)
   288  	if _, ok := d.files[name]; ok {
   289  		return fmt.Errorf("file %s already exists in the archive", name)
   290  	}
   291  	d.files[name] = path
   292  	return d.a.Add(name, path)
   293  }
   294  
   295  // Close closes the underlying archive.
   296  func (d EnhancedArchive) Close() error {
   297  	return d.a.Close()
   298  }