github.com/goreleaser/nfpm/v2@v2.44.0/arch/arch.go (about) 1 // Package arch implements nfpm.Packager providing bindings for Arch Linux packages. 2 package arch 3 4 import ( 5 "archive/tar" 6 "bytes" 7 "crypto/md5" 8 "crypto/sha256" 9 "errors" 10 "fmt" 11 "io" 12 "os" 13 "slices" 14 "strconv" 15 "strings" 16 "time" 17 18 "github.com/goreleaser/nfpm/v2" 19 "github.com/goreleaser/nfpm/v2/files" 20 "github.com/goreleaser/nfpm/v2/internal/maps" 21 "github.com/goreleaser/nfpm/v2/internal/modtime" 22 "github.com/klauspost/compress/zstd" 23 "github.com/klauspost/pgzip" 24 ) 25 26 var ErrInvalidPkgName = errors.New("archlinux: package names may only contain alphanumeric characters or one of ., _, +, or -, and may not start with hyphen or dot") 27 28 const packagerName = "archlinux" 29 30 // nolint: gochecknoinits 31 func init() { 32 nfpm.RegisterPackager(packagerName, Default) 33 } 34 35 // Default ArchLinux packager. 36 // nolint: gochecknoglobals 37 var Default = ArchLinux{} 38 39 // ArchLinux packager. 40 // nolint: revive 41 type ArchLinux struct{} 42 43 // nolint: gochecknoglobals 44 var archToArchLinux = map[string]string{ 45 "all": "any", 46 "amd64": "x86_64", 47 "386": "i686", 48 "arm64": "aarch64", 49 "arm7": "armv7h", 50 "arm6": "armv6h", 51 "arm5": "arm", 52 } 53 54 func ensureValidArch(info *nfpm.Info) *nfpm.Info { 55 if info.ArchLinux.Arch != "" { 56 info.Arch = info.ArchLinux.Arch 57 } else if arch, ok := archToArchLinux[info.Arch]; ok { 58 info.Arch = arch 59 } 60 61 return info 62 } 63 64 // ConventionalFileName returns a file name for a package conforming 65 // to Arch Linux package naming guidelines. See: 66 // https://wiki.archlinux.org/title/Arch_package_guidelines#Package_naming 67 func (ArchLinux) ConventionalFileName(info *nfpm.Info) string { 68 info = ensureValidArch(info) 69 70 pkgrel, err := strconv.Atoi(info.Release) 71 if err != nil { 72 pkgrel = 1 73 } 74 75 name := fmt.Sprintf( 76 "%s-%s-%d-%s.pkg.tar.zst", 77 info.Name, 78 info.Version+strings.ReplaceAll(info.Prerelease, "-", "_"), 79 pkgrel, 80 info.Arch, 81 ) 82 83 return validPkgName(name) 84 } 85 86 // validPkgName removes any invalid characters from a string 87 func validPkgName(s string) string { 88 s = strings.Map(mapValidChar, s) 89 s = strings.TrimLeft(s, "-.") 90 return s 91 } 92 93 // nameIsValid checks whether a package name is valid 94 func nameIsValid(s string) bool { 95 return s != "" && s == validPkgName(s) 96 } 97 98 // mapValidChar returns r if it is allowed, otherwise, returns -1 99 func mapValidChar(r rune) rune { 100 if r >= 'a' && r <= 'z' || 101 r >= 'A' && r <= 'Z' || 102 r >= '0' && r <= '9' || 103 isOneOf(r, '.', '_', '+', '-') { 104 return r 105 } 106 return -1 107 } 108 109 // isOneOf checks whether a rune is one of the runes in rr 110 func isOneOf(r rune, rr ...rune) bool { 111 return slices.Contains(rr, r) 112 } 113 114 // Package writes a new archlinux package to the given writer using the given info. 115 func (ArchLinux) Package(info *nfpm.Info, w io.Writer) error { 116 if info.Platform != "linux" { 117 return fmt.Errorf("invalid platform: %s", info.Platform) 118 } 119 info = ensureValidArch(info) 120 121 err := nfpm.PrepareForPackager(info, packagerName) 122 if err != nil { 123 return err 124 } 125 126 if !nameIsValid(info.Name) { 127 return ErrInvalidPkgName 128 } 129 130 zw, err := zstd.NewWriter(w) 131 if err != nil { 132 return err 133 } 134 defer zw.Close() 135 136 tw := tar.NewWriter(zw) 137 defer tw.Close() 138 139 entries, totalSize, err := createFilesInTar(info, tw) 140 if err != nil { 141 return fmt.Errorf("create files in tar: %w", err) 142 } 143 144 pkginfoEntry, err := createPkginfo(info, tw, totalSize) 145 if err != nil { 146 return fmt.Errorf("create pkg info: %w", err) 147 } 148 149 // .PKGINFO must be the first entry in .MTREE 150 entries = append([]MtreeEntry{*pkginfoEntry}, entries...) 151 152 err = createMtree(tw, entries, modtime.Get(info.MTime)) 153 if err != nil { 154 return fmt.Errorf("create mtree: %w", err) 155 } 156 157 return createScripts(info, tw) 158 } 159 160 // ConventionalExtension returns the file name conventionally used for Arch Linux packages 161 func (ArchLinux) ConventionalExtension() string { 162 return ".pkg.tar.zst" 163 } 164 165 // createFilesInTar adds the files described in the given info to the given tar writer 166 func createFilesInTar(info *nfpm.Info, tw *tar.Writer) ([]MtreeEntry, int64, error) { 167 entries := make([]MtreeEntry, 0, len(info.Contents)) 168 var totalSize int64 169 170 for _, content := range info.Contents { 171 content.Destination = files.AsRelativePath(content.Destination) 172 173 switch content.Type { 174 case files.TypeDir, files.TypeImplicitDir: 175 entries = append(entries, MtreeEntry{ 176 Destination: content.Destination, 177 Time: content.ModTime().Unix(), 178 Mode: int64(content.Mode()), 179 Type: files.TypeDir, 180 }) 181 182 if err := tw.WriteHeader(&tar.Header{ 183 Name: content.Destination, 184 Mode: int64(content.Mode()), 185 Typeflag: tar.TypeDir, 186 ModTime: content.ModTime(), 187 Uname: content.FileInfo.Owner, 188 Gname: content.FileInfo.Group, 189 }); err != nil { 190 return nil, 0, err 191 } 192 case files.TypeSymlink: 193 if err := tw.WriteHeader(&tar.Header{ 194 Name: content.Destination, 195 Linkname: content.Source, 196 ModTime: content.ModTime(), 197 Typeflag: tar.TypeSymlink, 198 }); err != nil { 199 return nil, 0, err 200 } 201 202 entries = append(entries, MtreeEntry{ 203 LinkSource: content.Source, 204 Destination: content.Destination, 205 Time: content.ModTime().Unix(), 206 Mode: 0o777, 207 Type: content.Type, 208 }) 209 default: 210 src, err := os.Open(content.Source) 211 if err != nil { 212 return nil, 0, err 213 } 214 defer src.Close() // nolint: errcheck 215 216 header := &tar.Header{ 217 Name: content.Destination, 218 Mode: int64(content.Mode()), 219 Typeflag: tar.TypeReg, 220 Size: content.Size(), 221 ModTime: content.ModTime(), 222 } 223 224 if content.FileInfo != nil && content.Mode() != 0 { 225 header.Mode = int64(content.Mode()) 226 } 227 228 if content.FileInfo != nil && !content.ModTime().IsZero() { 229 header.ModTime = content.ModTime() 230 } 231 232 if content.FileInfo != nil && content.Size() != 0 { 233 header.Size = content.Size() 234 } 235 236 err = tw.WriteHeader(header) 237 if err != nil { 238 return nil, 0, err 239 } 240 241 sha256Hash := sha256.New() 242 md5Hash := md5.New() 243 244 w := io.MultiWriter(tw, sha256Hash, md5Hash) 245 246 _, err = io.Copy(w, src) 247 if err != nil { 248 return nil, 0, err 249 } 250 251 entries = append(entries, MtreeEntry{ 252 Destination: content.Destination, 253 Time: content.ModTime().Unix(), 254 Mode: int64(content.Mode()), 255 Size: content.Size(), 256 Type: content.Type, 257 MD5: md5Hash.Sum(nil), 258 SHA256: sha256Hash.Sum(nil), 259 }) 260 261 totalSize += content.Size() 262 } 263 } 264 265 return entries, totalSize, nil 266 } 267 268 func defaultStr(s, def string) string { 269 if s == "" { 270 return def 271 } 272 return s 273 } 274 275 func createPkginfo(info *nfpm.Info, tw *tar.Writer, totalSize int64) (*MtreeEntry, error) { 276 if !nameIsValid(info.Name) { 277 return nil, ErrInvalidPkgName 278 } 279 280 buf := &bytes.Buffer{} 281 282 info = ensureValidArch(info) 283 284 pkgrel, err := strconv.Atoi(info.Release) 285 if err != nil { 286 pkgrel = 1 287 } 288 289 pkgver := fmt.Sprintf("%s-%d", info.Version, pkgrel) 290 if info.Epoch != "" { 291 epoch, err := strconv.ParseUint(info.Epoch, 10, 64) 292 if err == nil { 293 pkgver = fmt.Sprintf( 294 "%d:%s%s-%d", 295 epoch, 296 info.Version, 297 strings.ReplaceAll(info.Prerelease, "-", "_"), 298 pkgrel, 299 ) 300 } 301 } 302 303 // Description cannot contain newlines 304 pkgdesc := strings.ReplaceAll(info.Description, "\n", " ") 305 306 _, err = io.WriteString(buf, "# Generated by nfpm\n") 307 if err != nil { 308 return nil, err 309 } 310 311 builddate := strconv.FormatInt(modtime.Get(info.MTime).Unix(), 10) 312 totalSizeStr := strconv.FormatInt(totalSize, 10) 313 314 err = writeKVPairs(buf, map[string]string{ 315 "size": totalSizeStr, 316 "pkgname": info.Name, 317 "pkgbase": defaultStr(info.ArchLinux.Pkgbase, info.Name), 318 "pkgver": pkgver, 319 "pkgdesc": pkgdesc, 320 "url": info.Homepage, 321 "builddate": builddate, 322 "packager": defaultStr(info.ArchLinux.Packager, "Unknown Packager"), 323 "arch": info.Arch, 324 "license": info.License, 325 }) 326 if err != nil { 327 return nil, err 328 } 329 330 for _, replaces := range info.Replaces { 331 err = writeKVPair(buf, "replaces", replaces) 332 if err != nil { 333 return nil, err 334 } 335 } 336 337 for _, conflict := range info.Conflicts { 338 err = writeKVPair(buf, "conflict", conflict) 339 if err != nil { 340 return nil, err 341 } 342 } 343 344 for _, provides := range info.Provides { 345 err = writeKVPair(buf, "provides", provides) 346 if err != nil { 347 return nil, err 348 } 349 } 350 351 for _, depend := range info.Depends { 352 err = writeKVPair(buf, "depend", depend) 353 if err != nil { 354 return nil, err 355 } 356 } 357 358 for _, content := range info.Contents { 359 if content.Type == files.TypeConfig || content.Type == files.TypeConfigNoReplace || content.Type == files.TypeConfigMissingOK { 360 path := files.AsRelativePath(content.Destination) 361 362 if err := writeKVPair(buf, "backup", path); err != nil { 363 return nil, err 364 } 365 } 366 } 367 368 size := buf.Len() 369 370 err = tw.WriteHeader(&tar.Header{ 371 Typeflag: tar.TypeReg, 372 Mode: 0o644, 373 Name: ".PKGINFO", 374 Size: int64(size), 375 ModTime: modtime.Get(info.MTime), 376 }) 377 if err != nil { 378 return nil, err 379 } 380 381 md5Hash := md5.New() 382 sha256Hash := sha256.New() 383 384 r := io.TeeReader(buf, md5Hash) 385 r = io.TeeReader(r, sha256Hash) 386 387 if _, err = io.Copy(tw, r); err != nil { 388 return nil, err 389 } 390 391 return &MtreeEntry{ 392 Destination: ".PKGINFO", 393 Time: modtime.Get(info.MTime).Unix(), 394 Mode: 0o644, 395 Size: int64(size), 396 Type: files.TypeFile, 397 MD5: md5Hash.Sum(nil), 398 SHA256: sha256Hash.Sum(nil), 399 }, nil 400 } 401 402 func writeKVPairs(w io.Writer, pairs map[string]string) error { 403 for _, key := range maps.Keys(pairs) { 404 if err := writeKVPair(w, key, pairs[key]); err != nil { 405 return err 406 } 407 } 408 return nil 409 } 410 411 func writeKVPair(w io.Writer, key, value string) error { 412 if value == "" { 413 return nil 414 } 415 416 _, err := io.WriteString(w, key) 417 if err != nil { 418 return err 419 } 420 421 _, err = io.WriteString(w, " = ") 422 if err != nil { 423 return err 424 } 425 426 _, err = io.WriteString(w, value) 427 if err != nil { 428 return err 429 } 430 431 _, err = io.WriteString(w, "\n") 432 return err 433 } 434 435 type MtreeEntry struct { 436 LinkSource string 437 Destination string 438 Time int64 439 Mode int64 440 Size int64 441 Type string 442 MD5 []byte 443 SHA256 []byte 444 } 445 446 func (me *MtreeEntry) WriteTo(w io.Writer) (int64, error) { 447 switch me.Type { 448 case files.TypeDir, files.TypeImplicitDir: 449 n, err := fmt.Fprintf( 450 w, 451 "./%s time=%d.0 mode=%o type=dir\n", 452 me.Destination, 453 me.Time, 454 me.Mode, 455 ) 456 return int64(n), err 457 case files.TypeSymlink: 458 n, err := fmt.Fprintf( 459 w, 460 "./%s time=%d.0 mode=%o type=link link=%s\n", 461 me.Destination, 462 me.Time, 463 me.Mode, 464 me.LinkSource, 465 ) 466 return int64(n), err 467 default: 468 n, err := fmt.Fprintf( 469 w, 470 "./%s time=%d.0 mode=%o size=%d type=file md5digest=%x sha256digest=%x\n", 471 me.Destination, 472 me.Time, 473 me.Mode, 474 me.Size, 475 me.MD5, 476 me.SHA256, 477 ) 478 return int64(n), err 479 } 480 } 481 482 func createMtree(tw *tar.Writer, entries []MtreeEntry, mtime time.Time) error { 483 buf := &bytes.Buffer{} 484 gw := pgzip.NewWriter(buf) 485 defer gw.Close() 486 487 _, err := io.WriteString(gw, "#mtree\n") 488 if err != nil { 489 return err 490 } 491 492 for _, entry := range entries { 493 _, err = entry.WriteTo(gw) 494 if err != nil { 495 return err 496 } 497 } 498 499 gw.Close() 500 501 err = tw.WriteHeader(&tar.Header{ 502 Typeflag: tar.TypeReg, 503 Mode: 0o644, 504 Name: ".MTREE", 505 Size: int64(buf.Len()), 506 ModTime: mtime, 507 }) 508 if err != nil { 509 return err 510 } 511 512 _, err = io.Copy(tw, buf) 513 return err 514 } 515 516 func createScripts(info *nfpm.Info, tw *tar.Writer) error { 517 scripts := map[string]string{} 518 519 if info.Scripts.PreInstall != "" { 520 scripts["pre_install"] = info.Scripts.PreInstall 521 } 522 523 if info.Scripts.PostInstall != "" { 524 scripts["post_install"] = info.Scripts.PostInstall 525 } 526 527 if info.Scripts.PreRemove != "" { 528 scripts["pre_remove"] = info.Scripts.PreRemove 529 } 530 531 if info.Scripts.PostRemove != "" { 532 scripts["post_remove"] = info.Scripts.PostRemove 533 } 534 535 if info.ArchLinux.Scripts.PreUpgrade != "" { 536 scripts["pre_upgrade"] = info.ArchLinux.Scripts.PreUpgrade 537 } 538 539 if info.ArchLinux.Scripts.PostUpgrade != "" { 540 scripts["post_upgrade"] = info.ArchLinux.Scripts.PostUpgrade 541 } 542 543 if len(scripts) == 0 { 544 return nil 545 } 546 547 buf := &bytes.Buffer{} 548 549 err := writeScripts(buf, scripts) 550 if err != nil { 551 return err 552 } 553 554 err = tw.WriteHeader(&tar.Header{ 555 Typeflag: tar.TypeReg, 556 Mode: 0o644, 557 Name: ".INSTALL", 558 Size: int64(buf.Len()), 559 ModTime: modtime.Get(info.MTime), 560 }) 561 if err != nil { 562 return err 563 } 564 565 _, err = io.Copy(tw, buf) 566 return err 567 } 568 569 func writeScripts(w io.Writer, scripts map[string]string) error { 570 for _, script := range maps.Keys(scripts) { 571 fmt.Fprintf(w, "function %s() {\n", script) 572 573 fl, err := os.Open(scripts[script]) 574 if err != nil { 575 return err 576 } 577 defer fl.Close() //nolint: errcheck 578 579 _, err = io.Copy(w, fl) 580 if err != nil { 581 return err 582 } 583 584 _ = fl.Close() 585 586 _, err = io.WriteString(w, "\n}\n\n") 587 if err != nil { 588 return err 589 } 590 } 591 592 return nil 593 }