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