github.com/ahmet2mir/goreleaser@v0.180.3-0.20210927151101-8e5ee5a9b8c5/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  	"sort"
    12  	"strings"
    13  	"sync"
    14  
    15  	"github.com/apex/log"
    16  	"github.com/goreleaser/fileglob"
    17  
    18  	"github.com/goreleaser/goreleaser/internal/artifact"
    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  	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  	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  		if len(archive.Builds) == 0 {
    78  			for _, build := range ctx.Config.Builds {
    79  				archive.Builds = append(archive.Builds, build.ID)
    80  			}
    81  		}
    82  		ids.Inc(archive.ID)
    83  	}
    84  	return ids.Validate()
    85  }
    86  
    87  // Run the pipe.
    88  func (Pipe) Run(ctx *context.Context) error {
    89  	g := semerrgroup.New(ctx.Parallelism)
    90  	for i, archive := range ctx.Config.Archives {
    91  		archive := archive
    92  		artifacts := ctx.Artifacts.Filter(
    93  			artifact.And(
    94  				artifact.ByType(artifact.Binary),
    95  				artifact.ByIDs(archive.Builds...),
    96  			),
    97  		).GroupByPlatform()
    98  		if err := checkArtifacts(artifacts); err != nil && !archive.AllowDifferentBinaryCount {
    99  			return fmt.Errorf("invalid archive: %d: %w", i, ErrArchiveDifferentBinaryCount)
   100  		}
   101  		for group, artifacts := range artifacts {
   102  			log.Debugf("group %s has %d binaries", group, len(artifacts))
   103  			artifacts := artifacts
   104  			g.Go(func() error {
   105  				if packageFormat(archive, artifacts[0].Goos) == "binary" {
   106  					return skip(ctx, archive, artifacts)
   107  				}
   108  				return create(ctx, archive, artifacts)
   109  			})
   110  		}
   111  	}
   112  	return g.Wait()
   113  }
   114  
   115  func checkArtifacts(artifacts map[string][]*artifact.Artifact) error {
   116  	lens := map[int]bool{}
   117  	for _, v := range artifacts {
   118  		lens[len(v)] = true
   119  	}
   120  	if len(lens) <= 1 {
   121  		return nil
   122  	}
   123  	return ErrArchiveDifferentBinaryCount
   124  }
   125  
   126  func create(ctx *context.Context, arch config.Archive, binaries []*artifact.Artifact) error {
   127  	format := packageFormat(arch, binaries[0].Goos)
   128  	folder, err := tmpl.New(ctx).
   129  		WithArtifact(binaries[0], arch.Replacements).
   130  		Apply(arch.NameTemplate)
   131  	if err != nil {
   132  		return err
   133  	}
   134  	archivePath := filepath.Join(ctx.Config.Dist, folder+"."+format)
   135  	lock.Lock()
   136  	if err := os.MkdirAll(filepath.Dir(archivePath), 0o755|os.ModeDir); err != nil {
   137  		lock.Unlock()
   138  		return err
   139  	}
   140  	if _, err = os.Stat(archivePath); !os.IsNotExist(err) {
   141  		lock.Unlock()
   142  		return fmt.Errorf("archive named %s already exists. Check your archive name template", archivePath)
   143  	}
   144  	archiveFile, err := os.Create(archivePath)
   145  	if err != nil {
   146  		lock.Unlock()
   147  		return fmt.Errorf("failed to create directory %s: %w", archivePath, err)
   148  	}
   149  	lock.Unlock()
   150  	defer archiveFile.Close()
   151  
   152  	log := log.WithField("archive", archivePath)
   153  	log.Info("creating")
   154  
   155  	template := tmpl.New(ctx).
   156  		WithArtifact(binaries[0], arch.Replacements)
   157  	wrap, err := template.Apply(wrapFolder(arch))
   158  	if err != nil {
   159  		return err
   160  	}
   161  
   162  	a := NewEnhancedArchive(archive.New(archiveFile), wrap)
   163  	defer a.Close()
   164  
   165  	files, err := findFiles(template, arch.Files)
   166  	if err != nil {
   167  		return fmt.Errorf("failed to find files to archive: %w", err)
   168  	}
   169  	for _, f := range files {
   170  		if err = a.Add(f); err != nil {
   171  			return fmt.Errorf("failed to add: '%s' -> '%s': %w", f.Source, f.Destination, err)
   172  		}
   173  	}
   174  	for _, binary := range binaries {
   175  		if err := a.Add(config.File{
   176  			Source:      binary.Path,
   177  			Destination: binary.Name,
   178  		}); err != nil {
   179  			return fmt.Errorf("failed to add: '%s' -> '%s': %w", binary.Path, binary.Name, err)
   180  		}
   181  	}
   182  	ctx.Artifacts.Add(&artifact.Artifact{
   183  		Type:   artifact.UploadableArchive,
   184  		Name:   folder + "." + format,
   185  		Path:   archivePath,
   186  		Goos:   binaries[0].Goos,
   187  		Goarch: binaries[0].Goarch,
   188  		Goarm:  binaries[0].Goarm,
   189  		Gomips: binaries[0].Gomips,
   190  		Extra: map[string]interface{}{
   191  			"Builds":    binaries,
   192  			"ID":        arch.ID,
   193  			"Format":    arch.Format,
   194  			"WrappedIn": wrap,
   195  		},
   196  	})
   197  	return nil
   198  }
   199  
   200  func wrapFolder(a config.Archive) string {
   201  	switch a.WrapInDirectory {
   202  	case "true":
   203  		return a.NameTemplate
   204  	case "false":
   205  		return ""
   206  	default:
   207  		return a.WrapInDirectory
   208  	}
   209  }
   210  
   211  func skip(ctx *context.Context, archive config.Archive, binaries []*artifact.Artifact) error {
   212  	for _, binary := range binaries {
   213  		log.WithField("binary", binary.Name).Info("skip archiving")
   214  		name, err := tmpl.New(ctx).
   215  			WithArtifact(binary, archive.Replacements).
   216  			Apply(archive.NameTemplate)
   217  		if err != nil {
   218  			return err
   219  		}
   220  		ctx.Artifacts.Add(&artifact.Artifact{
   221  			Type:   artifact.UploadableBinary,
   222  			Name:   name + binary.ExtraOr("Ext", "").(string),
   223  			Path:   binary.Path,
   224  			Goos:   binary.Goos,
   225  			Goarch: binary.Goarch,
   226  			Goarm:  binary.Goarm,
   227  			Gomips: binary.Gomips,
   228  			Extra: map[string]interface{}{
   229  				"Builds": []*artifact.Artifact{binary},
   230  				"ID":     archive.ID,
   231  				"Format": archive.Format,
   232  			},
   233  		})
   234  	}
   235  	return nil
   236  }
   237  
   238  func findFiles(template *tmpl.Template, files []config.File) ([]config.File, error) {
   239  	var result []config.File
   240  	for _, f := range files {
   241  		replaced, err := template.Apply(f.Source)
   242  		if err != nil {
   243  			return result, fmt.Errorf("failed to apply template %s: %w", f.Source, err)
   244  		}
   245  
   246  		files, err := fileglob.Glob(replaced)
   247  		if err != nil {
   248  			return result, fmt.Errorf("globbing failed for pattern %s: %w", f.Source, err)
   249  		}
   250  
   251  		for _, file := range files {
   252  			result = append(result, config.File{
   253  				Source:      file,
   254  				Destination: destinationFor(f, file),
   255  				Info:        f.Info,
   256  			})
   257  		}
   258  	}
   259  
   260  	sort.Slice(result, func(i, j int) bool {
   261  		return result[i].Destination < result[j].Destination
   262  	})
   263  
   264  	return unique(result), nil
   265  }
   266  
   267  // remove duplicates
   268  func unique(in []config.File) []config.File {
   269  	var result []config.File
   270  	exist := map[string]string{}
   271  	for _, f := range in {
   272  		if current := exist[f.Destination]; current != "" {
   273  			log.Warnf(
   274  				"file '%s' already exists in archive as '%s' - '%s' will be ignored",
   275  				f.Destination,
   276  				current,
   277  				f.Source,
   278  			)
   279  			continue
   280  		}
   281  		exist[f.Destination] = f.Source
   282  		result = append(result, f)
   283  	}
   284  
   285  	return result
   286  }
   287  
   288  func destinationFor(f config.File, path string) string {
   289  	if f.Destination == "" {
   290  		return path
   291  	}
   292  	if f.StripParent {
   293  		return filepath.Join(f.Destination, filepath.Base(path))
   294  	}
   295  	return filepath.Join(f.Destination, path)
   296  }
   297  
   298  func packageFormat(archive config.Archive, platform string) string {
   299  	for _, override := range archive.FormatOverrides {
   300  		if strings.HasPrefix(platform, override.Goos) {
   301  			return override.Format
   302  		}
   303  	}
   304  	return archive.Format
   305  }
   306  
   307  // NewEnhancedArchive enhances a pre-existing archive.Archive instance
   308  // with this pipe specifics.
   309  func NewEnhancedArchive(a archive.Archive, wrap string) archive.Archive {
   310  	return EnhancedArchive{
   311  		a:     a,
   312  		wrap:  wrap,
   313  		files: map[string]string{},
   314  	}
   315  }
   316  
   317  // EnhancedArchive is an archive.Archive implementation which decorates an
   318  // archive.Archive adding wrap directory support, logging and windows
   319  // backslash fixes.
   320  type EnhancedArchive struct {
   321  	a     archive.Archive
   322  	wrap  string
   323  	files map[string]string
   324  }
   325  
   326  // Add adds a file.
   327  func (d EnhancedArchive) Add(f config.File) error {
   328  	name := strings.ReplaceAll(filepath.Join(d.wrap, f.Destination), "\\", "/")
   329  	log.Debugf("adding file: %s as %s", f.Source, name)
   330  	if _, ok := d.files[f.Destination]; ok {
   331  		return fmt.Errorf("file %s already exists in the archive", f.Destination)
   332  	}
   333  	d.files[f.Destination] = name
   334  	ff := config.File{
   335  		Source:      f.Source,
   336  		Destination: name,
   337  		Info:        f.Info,
   338  	}
   339  	return d.a.Add(ff)
   340  }
   341  
   342  // Close closes the underlying archive.
   343  func (d EnhancedArchive) Close() error {
   344  	return d.a.Close()
   345  }