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 }