1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-06 03:13:48 +02:00
goreleaser/internal/artifact/artifact.go
Carlos Alexandro Becker c83663cc26
fix: eventual race condition in artifacts (#3310)
* 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>
2022-08-16 02:05:36 -03:00

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
}