github.com/goreleaser/goreleaser@v1.25.1/internal/artifact/artifact.go (about)

     1  // Package artifact provides the core artifact storage for goreleaser.
     2  package artifact
     3  
     4  // nolint: gosec
     5  import (
     6  	"bytes"
     7  	"crypto/md5"
     8  	"crypto/sha1"
     9  	"crypto/sha256"
    10  	"crypto/sha512"
    11  	"encoding/hex"
    12  	"encoding/json"
    13  	"fmt"
    14  	"hash"
    15  	"hash/crc32"
    16  	"io"
    17  	"os"
    18  	"path/filepath"
    19  	"strings"
    20  	"sync"
    21  
    22  	"github.com/caarlos0/log"
    23  )
    24  
    25  // Type defines the type of an artifact.
    26  type Type int
    27  
    28  // If you add more types, update TestArtifactTypeStringer!
    29  const (
    30  	// UploadableArchive a tar.gz/zip archive to be uploaded.
    31  	UploadableArchive Type = iota + 1
    32  	// UploadableBinary is a binary file to be uploaded.
    33  	UploadableBinary
    34  	// UploadableFile is any file that can be uploaded.
    35  	UploadableFile
    36  	// Binary is a binary (output of a gobuild).
    37  	Binary
    38  	// UniversalBinary is a binary that contains multiple binaries within.
    39  	UniversalBinary
    40  	// LinuxPackage is a linux package generated by nfpm.
    41  	LinuxPackage
    42  	// PublishableSnapcraft is a snap package yet to be published.
    43  	PublishableSnapcraft
    44  	// Snapcraft is a published snap package.
    45  	Snapcraft
    46  	// PublishableDockerImage is a Docker image yet to be published.
    47  	PublishableDockerImage
    48  	// DockerImage is a published Docker image.
    49  	DockerImage
    50  	// DockerManifest is a published Docker manifest.
    51  	DockerManifest
    52  	// Checksum is a checksums file.
    53  	Checksum
    54  	// Signature is a signature file.
    55  	Signature
    56  	// Certificate is a signing certificate file
    57  	Certificate
    58  	// UploadableSourceArchive is the archive with the current commit source code.
    59  	UploadableSourceArchive
    60  	// BrewTap is an uploadable homebrew tap recipe file.
    61  	BrewTap
    62  	// Nixpkg is an uploadable nix package.
    63  	Nixpkg
    64  	// WingetInstaller winget installer file.
    65  	WingetInstaller
    66  	// WingetDefaultLocale winget default locale file.
    67  	WingetDefaultLocale
    68  	// WingetVersion winget version file.
    69  	WingetVersion
    70  	// PkgBuild is an Arch Linux AUR PKGBUILD file.
    71  	PkgBuild
    72  	// SrcInfo is an Arch Linux AUR .SRCINFO file.
    73  	SrcInfo
    74  	// KrewPluginManifest is a krew plugin manifest file.
    75  	KrewPluginManifest
    76  	// ScoopManifest is an uploadable scoop manifest file.
    77  	ScoopManifest
    78  	// SBOM is a Software Bill of Materials file.
    79  	SBOM
    80  	// PublishableChocolatey is a chocolatey package yet to be published.
    81  	PublishableChocolatey
    82  	// Header is a C header file, generated for CGo library builds.
    83  	Header
    84  	// CArchive is a C static library, generated via a CGo build with buildmode=c-archive.
    85  	CArchive
    86  	// CShared is a C shared library, generated via a CGo build with buildmode=c-shared.
    87  	CShared
    88  	// Metadata is an internal goreleaser metadata JSON file.
    89  	Metadata
    90  )
    91  
    92  func (t Type) String() string {
    93  	switch t {
    94  	case UploadableArchive:
    95  		return "Archive"
    96  	case UploadableFile:
    97  		return "File"
    98  	case UploadableBinary, Binary, UniversalBinary:
    99  		return "Binary"
   100  	case LinuxPackage:
   101  		return "Linux Package"
   102  	case PublishableDockerImage:
   103  		return "Docker Image"
   104  	case DockerImage:
   105  		return "Published Docker Image"
   106  	case DockerManifest:
   107  		return "Docker Manifest"
   108  	case PublishableSnapcraft, Snapcraft:
   109  		return "Snap"
   110  	case Checksum:
   111  		return "Checksum"
   112  	case Signature:
   113  		return "Signature"
   114  	case Certificate:
   115  		return "Certificate"
   116  	case UploadableSourceArchive:
   117  		return "Source"
   118  	case BrewTap:
   119  		return "Brew Tap"
   120  	case KrewPluginManifest:
   121  		return "Krew Plugin Manifest"
   122  	case ScoopManifest:
   123  		return "Scoop Manifest"
   124  	case SBOM:
   125  		return "SBOM"
   126  	case PkgBuild:
   127  		return "PKGBUILD"
   128  	case SrcInfo:
   129  		return "SRCINFO"
   130  	case PublishableChocolatey:
   131  		return "Chocolatey"
   132  	case Header:
   133  		return "C Header"
   134  	case CArchive:
   135  		return "C Archive Library"
   136  	case CShared:
   137  		return "C Shared Library"
   138  	case WingetInstaller, WingetDefaultLocale, WingetVersion:
   139  		return "Winget Manifest"
   140  	case Nixpkg:
   141  		return "Nixpkg"
   142  	case Metadata:
   143  		return "Metadata"
   144  	default:
   145  		return "unknown"
   146  	}
   147  }
   148  
   149  const (
   150  	ExtraID         = "ID"
   151  	ExtraBinary     = "Binary"
   152  	ExtraExt        = "Ext"
   153  	ExtraFormat     = "Format"
   154  	ExtraWrappedIn  = "WrappedIn"
   155  	ExtraBinaries   = "Binaries"
   156  	ExtraRefresh    = "Refresh"
   157  	ExtraReplaces   = "Replaces"
   158  	ExtraDigest     = "Digest"
   159  	ExtraSize       = "Size"
   160  	ExtraChecksum   = "Checksum"
   161  	ExtraChecksumOf = "ChecksumOf"
   162  )
   163  
   164  // Extras represents the extra fields in an artifact.
   165  type Extras map[string]any
   166  
   167  func (e Extras) MarshalJSON() ([]byte, error) {
   168  	m := map[string]any{}
   169  	for k, v := range e {
   170  		if k == ExtraRefresh {
   171  			// refresh is a func, so we can't serialize it.
   172  			continue
   173  		}
   174  		m[k] = v
   175  	}
   176  	return json.Marshal(m)
   177  }
   178  
   179  // Artifact represents an artifact and its relevant info.
   180  type Artifact struct {
   181  	Name    string `json:"name,omitempty"`
   182  	Path    string `json:"path,omitempty"`
   183  	Goos    string `json:"goos,omitempty"`
   184  	Goarch  string `json:"goarch,omitempty"`
   185  	Goarm   string `json:"goarm,omitempty"`
   186  	Gomips  string `json:"gomips,omitempty"`
   187  	Goamd64 string `json:"goamd64,omitempty"`
   188  	Type    Type   `json:"internal_type,omitempty"`
   189  	TypeS   string `json:"type,omitempty"`
   190  	Extra   Extras `json:"extra,omitempty"`
   191  }
   192  
   193  func (a Artifact) String() string {
   194  	return a.Name
   195  }
   196  
   197  // Extra tries to get the extra field with the given name, returning either
   198  // its value, the default value for its type, or an error.
   199  //
   200  // If the extra value cannot be cast into the given type, it'll try to convert
   201  // it to JSON and unmarshal it into the correct type after.
   202  //
   203  // If that fails as well, it'll error.
   204  func Extra[T any](a Artifact, key string) (T, error) {
   205  	ex := a.Extra[key]
   206  	if ex == nil {
   207  		return *(new(T)), nil
   208  	}
   209  
   210  	t, ok := ex.(T)
   211  	if ok {
   212  		return t, nil
   213  	}
   214  
   215  	bts, err := json.Marshal(ex)
   216  	if err != nil {
   217  		return t, err
   218  	}
   219  
   220  	decoder := json.NewDecoder(bytes.NewReader(bts))
   221  	decoder.DisallowUnknownFields()
   222  	err = decoder.Decode(&t)
   223  	return t, err
   224  }
   225  
   226  // ExtraOr returns the Extra field with the given key or the or value specified
   227  // if it is nil.
   228  func ExtraOr[T any](a Artifact, key string, or T) T {
   229  	if a.Extra[key] == nil {
   230  		return or
   231  	}
   232  	return a.Extra[key].(T)
   233  }
   234  
   235  // Checksum calculates the checksum of the artifact.
   236  // nolint: gosec
   237  func (a Artifact) Checksum(algorithm string) (string, error) {
   238  	log.Debugf("calculating checksum for %s", a.Path)
   239  	file, err := os.Open(a.Path)
   240  	if err != nil {
   241  		return "", fmt.Errorf("failed to checksum: %w", err)
   242  	}
   243  	defer file.Close()
   244  	var h hash.Hash
   245  	switch algorithm {
   246  	case "crc32":
   247  		h = crc32.NewIEEE()
   248  	case "md5":
   249  		h = md5.New()
   250  	case "sha224":
   251  		h = sha256.New224()
   252  	case "sha384":
   253  		h = sha512.New384()
   254  	case "sha256":
   255  		h = sha256.New()
   256  	case "sha1":
   257  		h = sha1.New()
   258  	case "sha512":
   259  		h = sha512.New()
   260  	default:
   261  		return "", fmt.Errorf("invalid algorithm: %s", algorithm)
   262  	}
   263  
   264  	if _, err := io.Copy(h, file); err != nil {
   265  		return "", fmt.Errorf("failed to checksum: %w", err)
   266  	}
   267  	check := hex.EncodeToString(h.Sum(nil))
   268  	if a.Extra == nil {
   269  		a.Extra = make(Extras)
   270  	}
   271  	a.Extra[ExtraChecksum] = fmt.Sprintf("%s:%s", algorithm, check)
   272  	return check, nil
   273  }
   274  
   275  var noRefresh = func() error { return nil }
   276  
   277  // Refresh executes a Refresh extra function on artifacts, if it exists.
   278  func (a Artifact) Refresh() error {
   279  	// for now lets only do it for checksums, as we know for a fact that
   280  	// they are the only ones that support this right now.
   281  	if a.Type != Checksum {
   282  		return nil
   283  	}
   284  	if err := ExtraOr(a, ExtraRefresh, noRefresh)(); err != nil {
   285  		return fmt.Errorf("failed to refresh %q: %w", a.Name, err)
   286  	}
   287  	return nil
   288  }
   289  
   290  // ID returns the artifact ID if it exists, empty otherwise.
   291  func (a Artifact) ID() string {
   292  	return ExtraOr(a, ExtraID, "")
   293  }
   294  
   295  // Format returns the artifact Format if it exists, empty otherwise.
   296  func (a Artifact) Format() string {
   297  	return ExtraOr(a, ExtraFormat, "")
   298  }
   299  
   300  // Artifacts is a list of artifacts.
   301  type Artifacts struct {
   302  	items []*Artifact
   303  	lock  *sync.Mutex
   304  }
   305  
   306  // New return a new list of artifacts.
   307  func New() *Artifacts {
   308  	return &Artifacts{
   309  		items: []*Artifact{},
   310  		lock:  &sync.Mutex{},
   311  	}
   312  }
   313  
   314  // Refresh visits all artifacts and refreshes them.
   315  func (artifacts *Artifacts) Refresh() error {
   316  	return artifacts.Visit(func(a *Artifact) error {
   317  		return a.Refresh()
   318  	})
   319  }
   320  
   321  // List return the actual list of artifacts.
   322  func (artifacts *Artifacts) List() []*Artifact {
   323  	artifacts.lock.Lock()
   324  	defer artifacts.lock.Unlock()
   325  	return artifacts.items
   326  }
   327  
   328  // GroupByID groups the artifacts by their ID.
   329  func (artifacts *Artifacts) GroupByID() map[string][]*Artifact {
   330  	result := map[string][]*Artifact{}
   331  	for _, a := range artifacts.List() {
   332  		id := a.ID()
   333  		if id == "" {
   334  			continue
   335  		}
   336  		result[a.ID()] = append(result[a.ID()], a)
   337  	}
   338  	return result
   339  }
   340  
   341  // GroupByPlatform groups the artifacts by their platform.
   342  func (artifacts *Artifacts) GroupByPlatform() map[string][]*Artifact {
   343  	result := map[string][]*Artifact{}
   344  	for _, a := range artifacts.List() {
   345  		plat := a.Goos + a.Goarch + a.Goarm + a.Gomips + a.Goamd64
   346  		result[plat] = append(result[plat], a)
   347  	}
   348  	return result
   349  }
   350  
   351  func relPath(a *Artifact) (string, error) {
   352  	cwd, err := os.Getwd()
   353  	if err != nil {
   354  		return "", err
   355  	}
   356  	if !strings.HasPrefix(a.Path, cwd) {
   357  		return "", nil
   358  	}
   359  	return filepath.Rel(cwd, a.Path)
   360  }
   361  
   362  func shouldRelPath(a *Artifact) bool {
   363  	switch a.Type {
   364  	case DockerImage, DockerManifest, PublishableDockerImage:
   365  		return false
   366  	default:
   367  		return filepath.IsAbs(a.Path)
   368  	}
   369  }
   370  
   371  // Add safely adds a new artifact to an artifact list.
   372  func (artifacts *Artifacts) Add(a *Artifact) {
   373  	artifacts.lock.Lock()
   374  	defer artifacts.lock.Unlock()
   375  	a.Name = cleanName(*a)
   376  	if shouldRelPath(a) {
   377  		rel, err := relPath(a)
   378  		if rel != "" && err == nil {
   379  			a.Path = rel
   380  		}
   381  	}
   382  	a.Path = filepath.ToSlash(a.Path)
   383  	log.WithField("name", a.Name).
   384  		WithField("type", a.Type).
   385  		WithField("path", a.Path).
   386  		Debug("added new artifact")
   387  	artifacts.items = append(artifacts.items, a)
   388  }
   389  
   390  // Remove removes artifacts that match the given filter from the original artifact list.
   391  func (artifacts *Artifacts) Remove(filter Filter) error {
   392  	if filter == nil {
   393  		return nil
   394  	}
   395  
   396  	artifacts.lock.Lock()
   397  	defer artifacts.lock.Unlock()
   398  
   399  	result := New()
   400  	for _, a := range artifacts.items {
   401  		if filter(a) {
   402  			log.WithField("name", a.Name).
   403  				WithField("type", a.Type).
   404  				WithField("path", a.Path).
   405  				Debug("removing")
   406  		} else {
   407  			result.items = append(result.items, a)
   408  		}
   409  	}
   410  
   411  	artifacts.items = result.items
   412  	return nil
   413  }
   414  
   415  // Filter defines an artifact filter which can be used within the Filter
   416  // function.
   417  type Filter func(a *Artifact) bool
   418  
   419  // OnlyReplacingUnibins removes universal binaries that did not replace the single-arch ones.
   420  //
   421  // This is useful specially on homebrew et al, where you'll want to use only either the single-arch or the universal binaries.
   422  func OnlyReplacingUnibins(a *Artifact) bool {
   423  	return ExtraOr(*a, ExtraReplaces, true)
   424  }
   425  
   426  // ByGoos is a predefined filter that filters by the given goos.
   427  func ByGoos(s string) Filter {
   428  	return func(a *Artifact) bool {
   429  		return a.Goos == s
   430  	}
   431  }
   432  
   433  // ByGoarch is a predefined filter that filters by the given goarch.
   434  func ByGoarch(s string) Filter {
   435  	return func(a *Artifact) bool {
   436  		return a.Goarch == s
   437  	}
   438  }
   439  
   440  // ByGoarm is a predefined filter that filters by the given goarm.
   441  func ByGoarm(s string) Filter {
   442  	return func(a *Artifact) bool {
   443  		return a.Goarm == s
   444  	}
   445  }
   446  
   447  // ByGoamd64 is a predefined filter that filters by the given goamd64.
   448  func ByGoamd64(s string) Filter {
   449  	return func(a *Artifact) bool {
   450  		return a.Goamd64 == s
   451  	}
   452  }
   453  
   454  // ByType is a predefined filter that filters by the given type.
   455  func ByType(t Type) Filter {
   456  	return func(a *Artifact) bool {
   457  		return a.Type == t
   458  	}
   459  }
   460  
   461  // ByFormats filters artifacts by a `Format` extra field.
   462  func ByFormats(formats ...string) Filter {
   463  	filters := make([]Filter, 0, len(formats))
   464  	for _, format := range formats {
   465  		format := format
   466  		filters = append(filters, func(a *Artifact) bool {
   467  			return a.Format() == format
   468  		})
   469  	}
   470  	return Or(filters...)
   471  }
   472  
   473  // ByIDs filter artifacts by an `ID` extra field.
   474  func ByIDs(ids ...string) Filter {
   475  	filters := make([]Filter, 0, len(ids))
   476  	for _, id := range ids {
   477  		id := id
   478  		filters = append(filters, func(a *Artifact) bool {
   479  			// checksum and source archive are always for all artifacts, so return always true.
   480  			return a.Type == Checksum ||
   481  				a.Type == UploadableSourceArchive ||
   482  				a.Type == UploadableFile ||
   483  				a.Type == Metadata ||
   484  				a.ID() == id
   485  		})
   486  	}
   487  	return Or(filters...)
   488  }
   489  
   490  // ByExt filter artifact by their 'Ext' extra field.
   491  func ByExt(exts ...string) Filter {
   492  	filters := make([]Filter, 0, len(exts))
   493  	for _, ext := range exts {
   494  		ext := ext
   495  		filters = append(filters, func(a *Artifact) bool {
   496  			return ExtraOr(*a, ExtraExt, "") == ext
   497  		})
   498  	}
   499  	return Or(filters...)
   500  }
   501  
   502  // ByBinaryLikeArtifacts filter artifacts down to artifacts that are Binary, UploadableBinary, or UniversalBinary,
   503  // deduplicating artifacts by path (preferring UploadableBinary over all others). Note: this filter is unique in the
   504  // sense that it cannot act in isolation of the state of other artifacts; the filter requires the whole list of
   505  // artifacts in advance to perform deduplication.
   506  func ByBinaryLikeArtifacts(arts *Artifacts) Filter {
   507  	// find all of the paths for any uploadable binary artifacts
   508  	uploadableBins := arts.Filter(ByType(UploadableBinary)).List()
   509  	uploadableBinPaths := map[string]struct{}{}
   510  	for _, a := range uploadableBins {
   511  		uploadableBinPaths[a.Path] = struct{}{}
   512  	}
   513  
   514  	// we want to keep any matching artifact that is not a binary that already has a path accounted for
   515  	// by another uploadable binary. We always prefer uploadable binary artifacts over binary artifacts.
   516  	deduplicateByPath := func(a *Artifact) bool {
   517  		if a.Type == UploadableBinary {
   518  			return true
   519  		}
   520  		_, ok := uploadableBinPaths[a.Path]
   521  		return !ok
   522  	}
   523  
   524  	return And(
   525  		// allow all of the binary-like artifacts as possible...
   526  		Or(
   527  			ByType(Binary),
   528  			ByType(UploadableBinary),
   529  			ByType(UniversalBinary),
   530  		),
   531  		// ... but remove any duplicates found
   532  		deduplicateByPath,
   533  	)
   534  }
   535  
   536  // Or performs an OR between all given filters.
   537  func Or(filters ...Filter) Filter {
   538  	return func(a *Artifact) bool {
   539  		for _, f := range filters {
   540  			if f(a) {
   541  				return true
   542  			}
   543  		}
   544  		return false
   545  	}
   546  }
   547  
   548  // And performs an AND between all given filters.
   549  func And(filters ...Filter) Filter {
   550  	return func(a *Artifact) bool {
   551  		for _, f := range filters {
   552  			if !f(a) {
   553  				return false
   554  			}
   555  		}
   556  		return true
   557  	}
   558  }
   559  
   560  // Filter filters the artifact list, returning a new instance.
   561  // There are some pre-defined filters but anything of the Type Filter
   562  // is accepted.
   563  // You can compose filters by using the And and Or filters.
   564  func (artifacts *Artifacts) Filter(filter Filter) *Artifacts {
   565  	if filter == nil {
   566  		return artifacts
   567  	}
   568  
   569  	result := New()
   570  	for _, a := range artifacts.List() {
   571  		if filter(a) {
   572  			result.items = append(result.items, a)
   573  		}
   574  	}
   575  	return result
   576  }
   577  
   578  // Paths returns the artifact.Path of the current artifact list.
   579  func (artifacts *Artifacts) Paths() []string {
   580  	var result []string
   581  	for _, artifact := range artifacts.List() {
   582  		result = append(result, artifact.Path)
   583  	}
   584  	return result
   585  }
   586  
   587  // VisitFn is a function that can be executed against each artifact in a list.
   588  type VisitFn func(a *Artifact) error
   589  
   590  // Visit executes the given function for each artifact in the list.
   591  func (artifacts *Artifacts) Visit(fn VisitFn) error {
   592  	for _, artifact := range artifacts.List() {
   593  		if err := fn(artifact); err != nil {
   594  			return err
   595  		}
   596  	}
   597  	return nil
   598  }
   599  
   600  func cleanName(a Artifact) string {
   601  	name := a.Name
   602  	ext := filepath.Ext(name)
   603  	result := strings.TrimSpace(strings.TrimSuffix(name, ext)) + ext
   604  	if name != result {
   605  		log.WithField("name", a.Name).
   606  			WithField("new name", result).
   607  			WithField("type", a.Type).
   608  			WithField("path", a.Path).
   609  			Warn("removed trailing whitespaces from artifact name")
   610  	}
   611  	return result
   612  }