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