mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-01-06 03:13:48 +02:00
c83663cc26
* fix: eventual race condition in artifacts Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com> * fix: locks Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com> * fix: tests Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com> Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
519 lines
13 KiB
Go
519 lines
13 KiB
Go
// Package artifact provides the core artifact storage for goreleaser.
|
|
package artifact
|
|
|
|
// nolint: gosec
|
|
import (
|
|
"bytes"
|
|
"crypto/md5"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"hash"
|
|
"hash/crc32"
|
|
"io"
|
|
"os"
|
|
"sync"
|
|
|
|
"github.com/caarlos0/log"
|
|
)
|
|
|
|
// Type defines the type of an artifact.
|
|
type Type int
|
|
|
|
const (
|
|
// UploadableArchive a tar.gz/zip archive to be uploaded.
|
|
UploadableArchive Type = iota + 1
|
|
// UploadableBinary is a binary file to be uploaded.
|
|
UploadableBinary
|
|
// UploadableFile is any file that can be uploaded.
|
|
UploadableFile
|
|
// Binary is a binary (output of a gobuild).
|
|
Binary
|
|
// UniversalBinary is a binary that contains multiple binaries within.
|
|
UniversalBinary
|
|
// LinuxPackage is a linux package generated by nfpm.
|
|
LinuxPackage
|
|
// PublishableSnapcraft is a snap package yet to be published.
|
|
PublishableSnapcraft
|
|
// Snapcraft is a published snap package.
|
|
Snapcraft
|
|
// PublishableDockerImage is a Docker image yet to be published.
|
|
PublishableDockerImage
|
|
// DockerImage is a published Docker image.
|
|
DockerImage
|
|
// DockerManifest is a published Docker manifest.
|
|
DockerManifest
|
|
// Checksum is a checksums file.
|
|
Checksum
|
|
// Signature is a signature file.
|
|
Signature
|
|
// Certificate is a signing certificate file
|
|
Certificate
|
|
// UploadableSourceArchive is the archive with the current commit source code.
|
|
UploadableSourceArchive
|
|
// BrewTap is an uploadable homebrew tap recipe file.
|
|
BrewTap
|
|
// PkgBuild is an Arch Linux AUR PKGBUILD file.
|
|
PkgBuild
|
|
// SrcInfo is an Arch Linux AUR .SRCINFO file.
|
|
SrcInfo
|
|
// KrewPluginManifest is a krew plugin manifest file.
|
|
KrewPluginManifest
|
|
// ScoopManifest is an uploadable scoop manifest file.
|
|
ScoopManifest
|
|
// SBOM is a Software Bill of Materials file.
|
|
SBOM
|
|
)
|
|
|
|
func (t Type) String() string {
|
|
switch t {
|
|
case UploadableArchive:
|
|
return "Archive"
|
|
case UploadableFile:
|
|
return "File"
|
|
case UploadableBinary, Binary, UniversalBinary:
|
|
return "Binary"
|
|
case LinuxPackage:
|
|
return "Linux Package"
|
|
case PublishableDockerImage, DockerImage:
|
|
return "Docker Image"
|
|
case DockerManifest:
|
|
return "Docker Manifest"
|
|
case PublishableSnapcraft, Snapcraft:
|
|
return "Snap"
|
|
case Checksum:
|
|
return "Checksum"
|
|
case Signature:
|
|
return "Signature"
|
|
case Certificate:
|
|
return "Certificate"
|
|
case UploadableSourceArchive:
|
|
return "Source"
|
|
case BrewTap:
|
|
return "Brew Tap"
|
|
case KrewPluginManifest:
|
|
return "Krew Plugin Manifest"
|
|
case ScoopManifest:
|
|
return "Scoop Manifest"
|
|
case SBOM:
|
|
return "SBOM"
|
|
case PkgBuild:
|
|
return "PKGBUILD"
|
|
case SrcInfo:
|
|
return "SRCINFO"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
const (
|
|
ExtraID = "ID"
|
|
ExtraBinary = "Binary"
|
|
ExtraExt = "Ext"
|
|
ExtraBuilds = "Builds"
|
|
ExtraFormat = "Format"
|
|
ExtraWrappedIn = "WrappedIn"
|
|
ExtraBinaries = "Binaries"
|
|
ExtraRefresh = "Refresh"
|
|
ExtraReplaces = "Replaces"
|
|
)
|
|
|
|
// Extras represents the extra fields in an artifact.
|
|
type Extras map[string]any
|
|
|
|
func (e Extras) MarshalJSON() ([]byte, error) {
|
|
m := map[string]any{}
|
|
for k, v := range e {
|
|
if k == ExtraRefresh {
|
|
// refresh is a func, so we can't serialize it.
|
|
continue
|
|
}
|
|
m[k] = v
|
|
}
|
|
return json.Marshal(m)
|
|
}
|
|
|
|
// Artifact represents an artifact and its relevant info.
|
|
type Artifact struct {
|
|
Name string `json:"name,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
Goos string `json:"goos,omitempty"`
|
|
Goarch string `json:"goarch,omitempty"`
|
|
Goarm string `json:"goarm,omitempty"`
|
|
Gomips string `json:"gomips,omitempty"`
|
|
Goamd64 string `json:"goamd64,omitempty"`
|
|
Type Type `json:"internal_type,omitempty"`
|
|
TypeS string `json:"type,omitempty"`
|
|
Extra Extras `json:"extra,omitempty"`
|
|
}
|
|
|
|
func (a Artifact) String() string {
|
|
return a.Name
|
|
}
|
|
|
|
// Extra tries to get the extra field with the given name, returning either
|
|
// its value, the default value for its type, or an error.
|
|
//
|
|
// If the extra value cannot be cast into the given type, it'll try to convert
|
|
// it to JSON and unmarshal it into the correct type after.
|
|
//
|
|
// If that fails as well, it'll error.
|
|
func Extra[T any](a Artifact, key string) (T, error) {
|
|
ex := a.Extra[key]
|
|
if ex == nil {
|
|
return *(new(T)), nil
|
|
}
|
|
|
|
t, ok := ex.(T)
|
|
if ok {
|
|
return t, nil
|
|
}
|
|
|
|
bts, err := json.Marshal(ex)
|
|
if err != nil {
|
|
return t, err
|
|
}
|
|
|
|
decoder := json.NewDecoder(bytes.NewReader(bts))
|
|
decoder.DisallowUnknownFields()
|
|
err = decoder.Decode(&t)
|
|
return t, err
|
|
}
|
|
|
|
// ExtraOr returns the Extra field with the given key or the or value specified
|
|
// if it is nil.
|
|
func ExtraOr[T any](a Artifact, key string, or T) T {
|
|
if a.Extra[key] == nil {
|
|
return or
|
|
}
|
|
return a.Extra[key].(T)
|
|
}
|
|
|
|
// Checksum calculates the checksum of the artifact.
|
|
// nolint: gosec
|
|
func (a Artifact) Checksum(algorithm string) (string, error) {
|
|
log.Debugf("calculating checksum for %s", a.Path)
|
|
file, err := os.Open(a.Path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to checksum: %w", err)
|
|
}
|
|
defer file.Close()
|
|
var h hash.Hash
|
|
switch algorithm {
|
|
case "crc32":
|
|
h = crc32.NewIEEE()
|
|
case "md5":
|
|
h = md5.New()
|
|
case "sha224":
|
|
h = sha256.New224()
|
|
case "sha384":
|
|
h = sha512.New384()
|
|
case "sha256":
|
|
h = sha256.New()
|
|
case "sha1":
|
|
h = sha1.New()
|
|
case "sha512":
|
|
h = sha512.New()
|
|
default:
|
|
return "", fmt.Errorf("invalid algorithm: %s", algorithm)
|
|
}
|
|
|
|
if _, err := io.Copy(h, file); err != nil {
|
|
return "", fmt.Errorf("failed to checksum: %w", err)
|
|
}
|
|
return hex.EncodeToString(h.Sum(nil)), nil
|
|
}
|
|
|
|
var noRefresh = func() error { return nil }
|
|
|
|
// Refresh executes a Refresh extra function on artifacts, if it exists.
|
|
func (a Artifact) Refresh() error {
|
|
// for now lets only do it for checksums, as we know for a fact that
|
|
// they are the only ones that support this right now.
|
|
if a.Type != Checksum {
|
|
return nil
|
|
}
|
|
if err := ExtraOr(a, ExtraRefresh, noRefresh)(); err != nil {
|
|
return fmt.Errorf("failed to refresh %q: %w", a.Name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ID returns the artifact ID if it exists, empty otherwise.
|
|
func (a Artifact) ID() string {
|
|
return ExtraOr(a, ExtraID, "")
|
|
}
|
|
|
|
// Format returns the artifact Format if it exists, empty otherwise.
|
|
func (a Artifact) Format() string {
|
|
return ExtraOr(a, ExtraFormat, "")
|
|
}
|
|
|
|
// Artifacts is a list of artifacts.
|
|
type Artifacts struct {
|
|
items []*Artifact
|
|
lock *sync.Mutex
|
|
}
|
|
|
|
// New return a new list of artifacts.
|
|
func New() Artifacts {
|
|
return Artifacts{
|
|
items: []*Artifact{},
|
|
lock: &sync.Mutex{},
|
|
}
|
|
}
|
|
|
|
// List return the actual list of artifacts.
|
|
func (artifacts Artifacts) List() []*Artifact {
|
|
artifacts.lock.Lock()
|
|
defer artifacts.lock.Unlock()
|
|
return artifacts.items
|
|
}
|
|
|
|
// GroupByID groups the artifacts by their ID.
|
|
func (artifacts Artifacts) GroupByID() map[string][]*Artifact {
|
|
result := map[string][]*Artifact{}
|
|
for _, a := range artifacts.List() {
|
|
id := a.ID()
|
|
if id == "" {
|
|
continue
|
|
}
|
|
result[a.ID()] = append(result[a.ID()], a)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GroupByPlatform groups the artifacts by their platform.
|
|
func (artifacts Artifacts) GroupByPlatform() map[string][]*Artifact {
|
|
result := map[string][]*Artifact{}
|
|
for _, a := range artifacts.List() {
|
|
plat := a.Goos + a.Goarch + a.Goarm + a.Gomips + a.Goamd64
|
|
result[plat] = append(result[plat], a)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Add safely adds a new artifact to an artifact list.
|
|
func (artifacts *Artifacts) Add(a *Artifact) {
|
|
artifacts.lock.Lock()
|
|
defer artifacts.lock.Unlock()
|
|
log.WithFields(log.Fields{
|
|
"name": a.Name,
|
|
"path": a.Path,
|
|
"type": a.Type,
|
|
}).Debug("added new artifact")
|
|
artifacts.items = append(artifacts.items, a)
|
|
}
|
|
|
|
// Remove removes artifacts that match the given filter from the original artifact list.
|
|
func (artifacts *Artifacts) Remove(filter Filter) error {
|
|
if filter == nil {
|
|
return nil
|
|
}
|
|
|
|
artifacts.lock.Lock()
|
|
defer artifacts.lock.Unlock()
|
|
|
|
result := New()
|
|
for _, a := range artifacts.items {
|
|
if filter(a) {
|
|
log.WithFields(log.Fields{
|
|
"name": a.Name,
|
|
"path": a.Path,
|
|
"type": a.Type,
|
|
}).Debug("removing")
|
|
} else {
|
|
result.items = append(result.items, a)
|
|
}
|
|
}
|
|
|
|
artifacts.items = result.items
|
|
return nil
|
|
}
|
|
|
|
// Filter defines an artifact filter which can be used within the Filter
|
|
// function.
|
|
type Filter func(a *Artifact) bool
|
|
|
|
// OnlyReplacingUnibins removes universal binaries that did not replace the single-arch ones.
|
|
//
|
|
// This is useful specially on homebrew et al, where you'll want to use only either the single-arch or the universal binaries.
|
|
func OnlyReplacingUnibins(a *Artifact) bool {
|
|
return ExtraOr(*a, ExtraReplaces, true)
|
|
}
|
|
|
|
// ByGoos is a predefined filter that filters by the given goos.
|
|
func ByGoos(s string) Filter {
|
|
return func(a *Artifact) bool {
|
|
return a.Goos == s
|
|
}
|
|
}
|
|
|
|
// ByGoarch is a predefined filter that filters by the given goarch.
|
|
func ByGoarch(s string) Filter {
|
|
return func(a *Artifact) bool {
|
|
return a.Goarch == s
|
|
}
|
|
}
|
|
|
|
// ByGoarm is a predefined filter that filters by the given goarm.
|
|
func ByGoarm(s string) Filter {
|
|
return func(a *Artifact) bool {
|
|
return a.Goarm == s
|
|
}
|
|
}
|
|
|
|
// ByGoamd64 is a predefined filter that filters by the given goamd64.
|
|
func ByGoamd64(s string) Filter {
|
|
return func(a *Artifact) bool {
|
|
return a.Goamd64 == s
|
|
}
|
|
}
|
|
|
|
// ByType is a predefined filter that filters by the given type.
|
|
func ByType(t Type) Filter {
|
|
return func(a *Artifact) bool {
|
|
return a.Type == t
|
|
}
|
|
}
|
|
|
|
// ByFormats filters artifacts by a `Format` extra field.
|
|
func ByFormats(formats ...string) Filter {
|
|
filters := make([]Filter, 0, len(formats))
|
|
for _, format := range formats {
|
|
format := format
|
|
filters = append(filters, func(a *Artifact) bool {
|
|
return a.Format() == format
|
|
})
|
|
}
|
|
return Or(filters...)
|
|
}
|
|
|
|
// ByIDs filter artifacts by an `ID` extra field.
|
|
func ByIDs(ids ...string) Filter {
|
|
filters := make([]Filter, 0, len(ids))
|
|
for _, id := range ids {
|
|
id := id
|
|
filters = append(filters, func(a *Artifact) bool {
|
|
// checksum and source archive are always for all artifacts, so return always true.
|
|
return a.Type == Checksum ||
|
|
a.Type == UploadableSourceArchive ||
|
|
a.ID() == id
|
|
})
|
|
}
|
|
return Or(filters...)
|
|
}
|
|
|
|
// ByExt filter artifact by their 'Ext' extra field.
|
|
func ByExt(exts ...string) Filter {
|
|
filters := make([]Filter, 0, len(exts))
|
|
for _, ext := range exts {
|
|
ext := ext
|
|
filters = append(filters, func(a *Artifact) bool {
|
|
return ExtraOr(*a, ExtraExt, "") == ext
|
|
})
|
|
}
|
|
return Or(filters...)
|
|
}
|
|
|
|
// ByBinaryLikeArtifacts filter artifacts down to artifacts that are Binary, UploadableBinary, or UniversalBinary,
|
|
// deduplicating artifacts by path (preferring UploadableBinary over all others). Note: this filter is unique in the
|
|
// sense that it cannot act in isolation of the state of other artifacts; the filter requires the whole list of
|
|
// artifacts in advance to perform deduplication.
|
|
func ByBinaryLikeArtifacts(arts Artifacts) Filter {
|
|
// find all of the paths for any uploadable binary artifacts
|
|
uploadableBins := arts.Filter(ByType(UploadableBinary)).List()
|
|
uploadableBinPaths := map[string]struct{}{}
|
|
for _, a := range uploadableBins {
|
|
uploadableBinPaths[a.Path] = struct{}{}
|
|
}
|
|
|
|
// we want to keep any matching artifact that is not a binary that already has a path accounted for
|
|
// by another uploadable binary. We always prefer uploadable binary artifacts over binary artifacts.
|
|
deduplicateByPath := func(a *Artifact) bool {
|
|
if a.Type == UploadableBinary {
|
|
return true
|
|
}
|
|
_, ok := uploadableBinPaths[a.Path]
|
|
return !ok
|
|
}
|
|
|
|
return And(
|
|
// allow all of the binary-like artifacts as possible...
|
|
Or(
|
|
ByType(Binary),
|
|
ByType(UploadableBinary),
|
|
ByType(UniversalBinary),
|
|
),
|
|
// ... but remove any duplicates found
|
|
deduplicateByPath,
|
|
)
|
|
}
|
|
|
|
// Or performs an OR between all given filters.
|
|
func Or(filters ...Filter) Filter {
|
|
return func(a *Artifact) bool {
|
|
for _, f := range filters {
|
|
if f(a) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
// And performs an AND between all given filters.
|
|
func And(filters ...Filter) Filter {
|
|
return func(a *Artifact) bool {
|
|
for _, f := range filters {
|
|
if !f(a) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Filter filters the artifact list, returning a new instance.
|
|
// There are some pre-defined filters but anything of the Type Filter
|
|
// is accepted.
|
|
// You can compose filters by using the And and Or filters.
|
|
func (artifacts *Artifacts) Filter(filter Filter) Artifacts {
|
|
if filter == nil {
|
|
return *artifacts
|
|
}
|
|
|
|
result := New()
|
|
for _, a := range artifacts.List() {
|
|
if filter(a) {
|
|
result.items = append(result.items, a)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Paths returns the artifact.Path of the current artifact list.
|
|
func (artifacts Artifacts) Paths() []string {
|
|
var result []string
|
|
for _, artifact := range artifacts.List() {
|
|
result = append(result, artifact.Path)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// VisitFn is a function that can be executed against each artifact in a list.
|
|
type VisitFn func(a *Artifact) error
|
|
|
|
// Visit executes the given function for each artifact in the list.
|
|
func (artifacts Artifacts) Visit(fn VisitFn) error {
|
|
for _, artifact := range artifacts.List() {
|
|
if err := fn(artifact); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|