1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-10 03:47:03 +02:00
goreleaser/internal/artifact/artifact.go
Carlos Alexandro Becker 2634fbdad4
fix: race condition on artifacts.List (#3813)
I have no idea why this never happened before... the lock was
ineffective in `artifacts.List`, which should have caused at least some
race condition at some point.

Anyway, got it once locally while working on another feature, and
couldn't believe my eyes.

Fixed, thank goodness!

Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
2023-03-01 01:05:30 -03:00

538 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
// PublishableChocolatey is a chocolatey package yet to be published.
PublishableChocolatey
// Header is a C header file, generated for CGo library builds.
Header
// CArchive is a C static library, generated via a CGo build with buildmode=c-archive.
CArchive
// CShared is a C shared library, generated via a CGo build with buildmode=c-shared.
CShared
)
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:
return "Docker Image"
case DockerImage:
return "Published 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"
case PublishableChocolatey:
return "Chocolatey"
case Header:
return "C Header"
case CArchive:
return "C Archive Library"
case CShared:
return "C Shared Library"
default:
return "unknown"
}
}
const (
ExtraID = "ID"
ExtraBinary = "Binary"
ExtraExt = "Ext"
ExtraBuilds = "Builds"
ExtraFormat = "Format"
ExtraWrappedIn = "WrappedIn"
ExtraBinaries = "Binaries"
ExtraRefresh = "Refresh"
ExtraReplaces = "Replaces"
ExtraDigest = "Digest"
)
// 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
}