github.com/goreleaser/nfpm/v2@v2.44.0/deb/deb.go (about) 1 // Package deb implements nfpm.Packager providing .deb bindings. 2 package deb 3 4 import ( 5 "archive/tar" 6 "bufio" 7 "bytes" 8 "compress/gzip" 9 "crypto/md5" // nolint:gas 10 "crypto/sha1" 11 "encoding/hex" 12 "errors" 13 "fmt" 14 "io" 15 "io/fs" 16 "os" 17 "path/filepath" 18 "strconv" 19 "strings" 20 "text/template" 21 "time" 22 23 "github.com/blakesmith/ar" 24 "github.com/goreleaser/chglog" 25 "github.com/goreleaser/nfpm/v2" 26 "github.com/goreleaser/nfpm/v2/deprecation" 27 "github.com/goreleaser/nfpm/v2/files" 28 "github.com/goreleaser/nfpm/v2/internal/maps" 29 "github.com/goreleaser/nfpm/v2/internal/modtime" 30 "github.com/goreleaser/nfpm/v2/internal/sign" 31 "github.com/klauspost/compress/zstd" 32 "github.com/ulikunitz/xz" 33 ) 34 35 const packagerName = "deb" 36 37 // nolint: gochecknoinits 38 func init() { 39 nfpm.RegisterPackager(packagerName, Default) 40 } 41 42 // nolint: gochecknoglobals 43 var archToDebian = map[string]string{ 44 "386": "i386", 45 "arm64": "arm64", 46 "arm5": "armel", 47 "arm6": "armhf", 48 "arm7": "armhf", 49 "mips64le": "mips64el", 50 "mipsle": "mipsel", 51 "ppc64le": "ppc64el", 52 "s390": "s390x", 53 } 54 55 func ensureValidArch(info *nfpm.Info) *nfpm.Info { 56 if info.Deb.Arch != "" { 57 info.Arch = info.Deb.Arch 58 } else if arch, ok := archToDebian[info.Arch]; ok { 59 info.Arch = arch 60 } 61 62 return info 63 } 64 65 // Default deb packager. 66 // nolint: gochecknoglobals 67 var Default = &Deb{} 68 69 // Deb is a deb packager implementation. 70 type Deb struct{} 71 72 // ConventionalFileName returns a file name according 73 // to the conventions for debian packages. See: 74 // https://manpages.debian.org/buster/dpkg-dev/dpkg-name.1.en.html 75 func (*Deb) ConventionalFileName(info *nfpm.Info) string { 76 info = ensureValidArch(info) 77 78 version := info.Version 79 if info.Prerelease != "" { 80 version += "~" + info.Prerelease 81 } 82 83 if info.VersionMetadata != "" { 84 version += "+" + info.VersionMetadata 85 } 86 87 if info.Release != "" { 88 version += "-" + info.Release 89 } 90 91 // package_version_architecture.package-type 92 return fmt.Sprintf("%s_%s_%s.deb", info.Name, version, info.Arch) 93 } 94 95 // ConventionalExtension returns the file name conventionally used for Deb packages 96 func (*Deb) ConventionalExtension() string { 97 return ".deb" 98 } 99 100 // ErrInvalidSignatureType happens if the signature type of a deb is not one of 101 // origin, maint or archive. 102 var ErrInvalidSignatureType = errors.New("invalid signature type") 103 104 // Package writes a new deb package to the given writer using the given info. 105 func (d *Deb) Package(info *nfpm.Info, deb io.Writer) (err error) { // nolint: funlen 106 info = ensureValidArch(info) 107 108 err = nfpm.PrepareForPackager(withChangelogIfRequested(info), packagerName) 109 if err != nil { 110 return err 111 } 112 113 // Set up some deb specific defaults 114 d.SetPackagerDefaults(info) 115 116 dataTarball, md5sums, instSize, dataTarballName, err := createDataTarball(info) 117 if err != nil { 118 return err 119 } 120 121 controlTarGz, err := createControl(instSize, md5sums, info) 122 if err != nil { 123 return err 124 } 125 126 debianBinary := []byte("2.0\n") 127 128 w := ar.NewWriter(deb) 129 if err := w.WriteGlobalHeader(); err != nil { 130 return fmt.Errorf("cannot write ar header to deb file: %w", err) 131 } 132 133 mtime := modtime.Get(info.MTime) 134 135 if err := addArFile(w, "debian-binary", debianBinary, mtime); err != nil { 136 return fmt.Errorf("cannot pack debian-binary: %w", err) 137 } 138 139 if err := addArFile(w, "control.tar.gz", controlTarGz, mtime); err != nil { 140 return fmt.Errorf("cannot add control.tar.gz to deb: %w", err) 141 } 142 143 if err := addArFile(w, dataTarballName, dataTarball, mtime); err != nil { 144 return fmt.Errorf("cannot add data.tar.gz to deb: %w", err) 145 } 146 147 if info.Deb.Signature.KeyFile != "" || info.Deb.Signature.SignFn != nil { 148 sig, sigType, err := doSign(info, debianBinary, controlTarGz, dataTarball) 149 if err != nil { 150 return err 151 } 152 153 if err := addArFile(w, "_gpg"+sigType, sig, mtime); err != nil { 154 return &nfpm.ErrSigningFailure{ 155 Err: fmt.Errorf("add signature to ar file: %w", err), 156 } 157 } 158 } 159 160 return nil 161 } 162 163 func doSign(info *nfpm.Info, debianBinary, controlTarGz, dataTarball []byte) ([]byte, string, error) { 164 switch info.Deb.Signature.Method { 165 case "dpkg-sig": 166 return dpkgSign(info, debianBinary, controlTarGz, dataTarball) 167 default: 168 return debSign(info, debianBinary, controlTarGz, dataTarball) 169 } 170 } 171 172 func dpkgSign(info *nfpm.Info, debianBinary, controlTarGz, dataTarball []byte) ([]byte, string, error) { 173 sigType := "builder" 174 if info.Deb.Signature.Type != "" { 175 sigType = info.Deb.Signature.Type 176 } 177 178 data, err := readDpkgSigData(info, debianBinary, controlTarGz, dataTarball) 179 if err != nil { 180 return nil, sigType, &nfpm.ErrSigningFailure{Err: err} 181 } 182 183 var sig []byte 184 if signFn := info.Deb.Signature.SignFn; signFn != nil { 185 sig, err = signFn(data) 186 } else { 187 sig, err = sign.PGPClearSignWithKeyID(data, info.Deb.Signature.KeyFile, info.Deb.Signature.KeyPassphrase, info.Deb.Signature.KeyID) 188 } 189 if err != nil { 190 return nil, sigType, &nfpm.ErrSigningFailure{Err: err} 191 } 192 return sig, sigType, nil 193 } 194 195 func debSign(info *nfpm.Info, debianBinary, controlTarGz, dataTarball []byte) ([]byte, string, error) { 196 data := readDebsignData(debianBinary, controlTarGz, dataTarball) 197 198 sigType := "origin" 199 if info.Deb.Signature.Type != "" { 200 sigType = info.Deb.Signature.Type 201 } 202 203 if sigType != "origin" && sigType != "maint" && sigType != "archive" { 204 return nil, sigType, &nfpm.ErrSigningFailure{ 205 Err: ErrInvalidSignatureType, 206 } 207 } 208 209 var sig []byte 210 var err error 211 if signFn := info.Deb.Signature.SignFn; signFn != nil { 212 sig, err = signFn(data) 213 } else { 214 sig, err = sign.PGPArmoredDetachSignWithKeyID(data, info.Deb.Signature.KeyFile, info.Deb.Signature.KeyPassphrase, info.Deb.Signature.KeyID) 215 } 216 if err != nil { 217 return nil, sigType, &nfpm.ErrSigningFailure{Err: err} 218 } 219 return sig, sigType, nil 220 } 221 222 func readDebsignData(debianBinary, controlTarGz, dataTarball []byte) io.Reader { 223 return io.MultiReader(bytes.NewReader(debianBinary), bytes.NewReader(controlTarGz), 224 bytes.NewReader(dataTarball)) 225 } 226 227 // reference: https://manpages.debian.org/jessie/dpkg-sig/dpkg-sig.1.en.html 228 const dpkgSigTemplate = ` 229 Hash: SHA1 230 231 Version: 4 232 Signer: {{ .Signer }} 233 Date: {{ .Date }} 234 Role: {{ .Role }} 235 Files: 236 {{range .Files -}} 237 {{"\t"}}{{ hex .Md5Sum }} {{ hex .Sha1Sum }} {{ .Size }} {{ .Name }} 238 {{end -}} 239 ` 240 241 type dpkgSigData struct { 242 Signer string 243 Date time.Time 244 Role string 245 Files []dpkgSigFileLine 246 Info *nfpm.Info 247 } 248 type dpkgSigFileLine struct { 249 Md5Sum []byte 250 Sha1Sum []byte 251 Size int 252 Name string 253 } 254 255 func newDpkgSigFileLine(name string, fileContent []byte) dpkgSigFileLine { 256 md5Sum, sha1Sum := md5.Sum(fileContent), sha1.Sum(fileContent) 257 return dpkgSigFileLine{ 258 Name: name, 259 Md5Sum: md5Sum[:], 260 Sha1Sum: sha1Sum[:], 261 Size: len(fileContent), 262 } 263 } 264 265 func readDpkgSigData(info *nfpm.Info, debianBinary, controlTarGz, dataTarball []byte) (io.Reader, error) { 266 data := dpkgSigData{ 267 Signer: info.Deb.Signature.Signer, 268 Date: modtime.Get(info.MTime), 269 Role: info.Deb.Signature.Type, 270 Files: []dpkgSigFileLine{ 271 newDpkgSigFileLine("debian-binary", debianBinary), 272 newDpkgSigFileLine("control.tar.gz", controlTarGz), 273 newDpkgSigFileLine("data.tar.gz", dataTarball), 274 }, 275 } 276 temp, _ := template.New("dpkg-sig").Funcs(template.FuncMap{ 277 "hex": hex.EncodeToString, 278 }).Parse(dpkgSigTemplate) 279 buf := &bytes.Buffer{} 280 err := temp.Execute(buf, data) 281 if err != nil { 282 return nil, fmt.Errorf("dpkg-sig template error: %w", err) 283 } 284 return buf, nil 285 } 286 287 func (*Deb) SetPackagerDefaults(info *nfpm.Info) { 288 // Priority should be set on all packages per: 289 // https://www.debian.org/doc/debian-policy/ch-archive.html#priorities 290 // "optional" seems to be the safe/sane default here 291 if info.Priority == "" { 292 info.Priority = "optional" 293 } 294 295 // The safe thing here feels like defaulting to something like below. 296 // That will prevent existing configs from breaking anyway... Wondering 297 // if in the long run we should be more strict about this and error when 298 // not set? 299 if info.Maintainer == "" { 300 deprecation.Println("Leaving the 'maintainer' field unset will not be allowed in a future version") 301 info.Maintainer = "Unset Maintainer <unset@localhost>" 302 } 303 } 304 305 func addArFile(w *ar.Writer, name string, body []byte, date time.Time) error { 306 header := ar.Header{ 307 Name: files.ToNixPath(name), 308 Size: int64(len(body)), 309 Mode: 0o644, 310 ModTime: date, 311 } 312 if err := w.WriteHeader(&header); err != nil { 313 return fmt.Errorf("cannot write file header: %w", err) 314 } 315 _, err := w.Write(body) 316 return err 317 } 318 319 type nopCloser struct { 320 io.Writer 321 } 322 323 func (nopCloser) Close() error { return nil } 324 325 func createDataTarball(info *nfpm.Info) (dataTarBall, md5sums []byte, 326 instSize int64, name string, err error, 327 ) { 328 var ( 329 dataTarball bytes.Buffer 330 dataTarballWriteCloser io.WriteCloser 331 ) 332 333 if info.Deb.Compression == "" { 334 info.Deb.Compression = "gzip:-1" // the default for now 335 } 336 337 parts := strings.Split(info.Deb.Compression, ":") 338 if len(parts) > 2 { 339 return nil, nil, 0, "", fmt.Errorf("malformed compressor setting: %s", info.Deb.Compression) 340 } 341 342 compressorType := parts[0] 343 compressorLevel := "" 344 if len(parts) == 2 { 345 compressorLevel = parts[1] 346 } 347 348 switch compressorType { 349 case "gzip": 350 level := 9 351 if compressorLevel != "" { 352 var err error 353 level, err = strconv.Atoi(compressorLevel) 354 if err != nil { 355 return nil, nil, 0, "", fmt.Errorf("parse gzip compressor level: %w", err) 356 } 357 } 358 dataTarballWriteCloser, err = gzip.NewWriterLevel(&dataTarball, level) 359 if err != nil { 360 return nil, nil, 0, "", err 361 } 362 name = "data.tar.gz" 363 case "xz": 364 if compressorLevel != "" { 365 return nil, nil, 0, "", fmt.Errorf("no compressor level supported for xz: %s", compressorLevel) 366 } 367 dataTarballWriteCloser, err = xz.NewWriter(&dataTarball) 368 if err != nil { 369 return nil, nil, 0, "", err 370 } 371 name = "data.tar.xz" 372 case "zstd": 373 level := zstd.SpeedBetterCompression 374 if compressorLevel != "" { 375 if intLevel, err := strconv.Atoi(compressorLevel); err == nil { 376 level = zstd.EncoderLevelFromZstd(intLevel) 377 } else { 378 var ok bool 379 ok, level = zstd.EncoderLevelFromString(compressorLevel) 380 if !ok { 381 return nil, nil, 0, "", fmt.Errorf("invalid zstd compressor level: %s", compressorLevel) 382 } 383 } 384 } 385 dataTarballWriteCloser, err = zstd.NewWriter(&dataTarball, zstd.WithEncoderLevel(level)) 386 if err != nil { 387 return nil, nil, 0, "", err 388 } 389 name = "data.tar.zst" 390 case "none": 391 dataTarballWriteCloser = nopCloser{Writer: &dataTarball} 392 name = "data.tar" 393 default: 394 return nil, nil, 0, "", fmt.Errorf("unknown compression algorithm: %s", info.Deb.Compression) 395 } 396 397 // the writer is properly closed later, this is just in case that we error out 398 defer dataTarballWriteCloser.Close() // nolint: errcheck 399 400 md5sums, instSize, err = fillDataTar(info, dataTarballWriteCloser) 401 if err != nil { 402 return nil, nil, 0, "", err 403 } 404 405 if err := dataTarballWriteCloser.Close(); err != nil { 406 return nil, nil, 0, "", fmt.Errorf("closing data tarball: %w", err) 407 } 408 409 return dataTarball.Bytes(), md5sums, instSize, name, nil 410 } 411 412 func fillDataTar(info *nfpm.Info, w io.Writer) (md5sums []byte, instSize int64, err error) { 413 out := tar.NewWriter(w) 414 415 // the writer is properly closed later, this is just in case that we have 416 // an error in another part of the code. 417 defer out.Close() // nolint: errcheck 418 419 md5buf, instSize, err := createFilesInsideDataTar(info, out) 420 if err != nil { 421 return nil, 0, err 422 } 423 424 if err := out.Close(); err != nil { 425 return nil, 0, fmt.Errorf("closing data.tar.gz: %w", err) 426 } 427 428 return md5buf.Bytes(), instSize, nil 429 } 430 431 func createFilesInsideDataTar(info *nfpm.Info, tw *tar.Writer) (md5buf bytes.Buffer, instSize int64, err error) { 432 for _, file := range info.Contents { 433 switch file.Type { 434 case files.TypeRPMGhost: 435 continue // skip ghost files in deb 436 case files.TypeDir, files.TypeImplicitDir: 437 header, err := tarHeader(file, info.MTime) 438 if err != nil { 439 return md5buf, 0, fmt.Errorf("build directory header for %q: %w", 440 file.Destination, err) 441 } 442 443 err = tw.WriteHeader(header) 444 if err != nil { 445 return md5buf, 0, fmt.Errorf("create directory %q in data tar: %w", 446 header.Name, err) 447 } 448 case files.TypeSymlink: 449 header, err := tarHeader(file, info.MTime) 450 if err != nil { 451 return md5buf, 0, fmt.Errorf("build symlink header for %q: %w", 452 file.Destination, err) 453 } 454 455 err = newItemInsideTar(tw, []byte{}, header) 456 if err != nil { 457 return md5buf, 0, fmt.Errorf("create symlink %q in data tar: %w", 458 header.Linkname, err) 459 } 460 case files.TypeDebChangelog: 461 size, err := createChangelogInsideDataTar(tw, &md5buf, info, file.Destination) 462 if err != nil { 463 return md5buf, 0, fmt.Errorf("write changelog to data tar: %w", err) 464 } 465 466 instSize += size 467 default: 468 size, err := copyToTarAndDigest(file, tw, &md5buf) 469 if err != nil { 470 return md5buf, 0, fmt.Errorf("write %q to data tar: %w", file.Destination, err) 471 } 472 473 instSize += size 474 } 475 } 476 477 return md5buf, instSize, nil 478 } 479 480 func copyToTarAndDigest(file *files.Content, tw *tar.Writer, md5w io.Writer) (int64, error) { 481 tarFile, err := os.OpenFile(file.Source, os.O_RDONLY, 0o600) //nolint:gosec 482 if err != nil { 483 return 0, fmt.Errorf("could not add tarFile to the archive: %w", err) 484 } 485 // don't care if it errs while closing... 486 defer tarFile.Close() // nolint: errcheck,gosec 487 488 header, err := tarHeader(file) 489 if err != nil { 490 return 0, err 491 } 492 493 if err := tw.WriteHeader(header); err != nil { 494 return 0, fmt.Errorf("cannot write header of %s to data.tar.gz: %w", file.Source, err) 495 } 496 digest := md5.New() // nolint:gas 497 if _, err := io.Copy(tw, io.TeeReader(tarFile, digest)); err != nil { 498 return 0, fmt.Errorf("%s: failed to copy: %w", file.Source, err) 499 } 500 if _, err := fmt.Fprintf(md5w, "%x %s\n", digest.Sum(nil), header.Name); err != nil { 501 return 0, fmt.Errorf("%s: failed to write md5: %w", file.Source, err) 502 } 503 return file.Size(), nil 504 } 505 506 func withChangelogIfRequested(info *nfpm.Info) *nfpm.Info { 507 if info.Changelog == "" { 508 return info 509 } 510 511 // https://www.debian.org/doc/manuals/developers-reference/pkgs.de.html#recording-changes-in-the-package 512 // https://lintian.debian.org/tags/debian-changelog-file-missing-or-wrong-name 513 info.Contents = append(info.Contents, &files.Content{ 514 Destination: fmt.Sprintf("/usr/share/doc/%s/changelog.Debian.gz", info.Name), 515 Type: files.TypeDebChangelog, // this type is handeled in createDataTarball 516 }) 517 518 return info 519 } 520 521 func createChangelogInsideDataTar( 522 tarw *tar.Writer, 523 g io.Writer, 524 info *nfpm.Info, 525 fileName string, 526 ) (int64, error) { 527 var buf bytes.Buffer 528 // we need here a non timestamped compression -> https://github.com/klauspost/pgzip doesn't support that 529 // https://github.com/klauspost/pgzip/blob/v1.2.6/gzip.go#L322 vs. 530 // https://cs.opensource.google/go/go/+/refs/tags/go1.20.4:src/compress/gzip/gzip.go;l=157 531 out, err := gzip.NewWriterLevel(&buf, gzip.BestCompression) 532 if err != nil { 533 return 0, fmt.Errorf("could not create gzip writer: %w", err) 534 } 535 // the writers are properly closed later, this is just in case that we have 536 // an error in another part of the code. 537 defer out.Close() // nolint: errcheck 538 539 changelogContent, err := formatChangelog(info) 540 if err != nil { 541 return 0, err 542 } 543 544 if _, err = io.WriteString(out, changelogContent); err != nil { 545 return 0, err 546 } 547 548 if err = out.Close(); err != nil { 549 return 0, fmt.Errorf("closing %s: %w", filepath.Base(fileName), err) 550 } 551 552 changelogData := buf.Bytes() 553 554 digest := md5.New() // nolint:gas 555 if _, err = digest.Write(changelogData); err != nil { 556 return 0, err 557 } 558 559 if _, err = fmt.Fprintf( 560 g, 561 "%x %s\n", 562 digest.Sum(nil), 563 files.AsExplicitRelativePath(fileName), 564 ); err != nil { 565 return 0, err 566 } 567 568 if err = newFileInsideTar(tarw, fileName, changelogData, modtime.Get(info.MTime)); err != nil { 569 return 0, err 570 } 571 572 return int64(len(changelogData)), nil 573 } 574 575 func formatChangelog(info *nfpm.Info) (string, error) { 576 changelog, err := info.GetChangeLog() 577 if err != nil { 578 return "", err 579 } 580 581 tpl, err := chglog.DebTemplate() 582 if err != nil { 583 return "", err 584 } 585 586 formattedChangelog, err := chglog.FormatChangelog(changelog, tpl) 587 if err != nil { 588 return "", err 589 } 590 591 return strings.TrimSpace(formattedChangelog) + "\n", nil 592 } 593 594 // nolint:funlen 595 func createControl(instSize int64, md5sums []byte, info *nfpm.Info) (controlTarGz []byte, err error) { 596 var buf bytes.Buffer 597 compress := gzip.NewWriter(&buf) 598 out := tar.NewWriter(compress) 599 // the writers are properly closed later, this is just in case that we have 600 // an error in another part of the code. 601 defer out.Close() // nolint: errcheck 602 defer compress.Close() // nolint: errcheck 603 604 var body bytes.Buffer 605 if err = writeControl(&body, controlData{ 606 Info: info, 607 InstalledSize: instSize / 1024, 608 }); err != nil { 609 return nil, err 610 } 611 612 mtime := modtime.Get(info.MTime) 613 if err := newFileInsideTar(out, "./control", body.Bytes(), mtime); err != nil { 614 return nil, err 615 } 616 if err := newFileInsideTar(out, "./md5sums", md5sums, mtime); err != nil { 617 return nil, err 618 } 619 if conffiles, ok := conffiles(info); ok { 620 if err := newFileInsideTar(out, "./conffiles", conffiles, mtime); err != nil { 621 return nil, err 622 } 623 } 624 625 if triggers := createTriggers(info); len(triggers) > 0 { 626 if err := newFileInsideTar(out, "./triggers", triggers, mtime); err != nil { 627 return nil, err 628 } 629 } 630 631 type fileAndMode struct { 632 fileName string 633 mode int64 634 } 635 636 specialFiles := map[string]*fileAndMode{ 637 "preinst": { 638 fileName: info.Scripts.PreInstall, 639 mode: 0o755, 640 }, 641 "postinst": { 642 fileName: info.Scripts.PostInstall, 643 mode: 0o755, 644 }, 645 "prerm": { 646 fileName: info.Scripts.PreRemove, 647 mode: 0o755, 648 }, 649 "postrm": { 650 fileName: info.Scripts.PostRemove, 651 mode: 0o755, 652 }, 653 "rules": { 654 fileName: info.Deb.Scripts.Rules, 655 mode: 0o755, 656 }, 657 "templates": { 658 fileName: info.Deb.Scripts.Templates, 659 mode: 0o644, 660 }, 661 "config": { 662 fileName: info.Deb.Scripts.Config, 663 mode: 0o755, 664 }, 665 } 666 667 for _, filename := range maps.Keys(specialFiles) { 668 dets := specialFiles[filename] 669 if dets.fileName == "" { 670 continue 671 } 672 if err := newFilePathInsideTar(out, dets.fileName, filename, dets.mode, mtime); err != nil { 673 return nil, err 674 } 675 } 676 677 if err := out.Close(); err != nil { 678 return nil, fmt.Errorf("closing control.tar.gz: %w", err) 679 } 680 if err := compress.Close(); err != nil { 681 return nil, fmt.Errorf("closing control.tar.gz: %w", err) 682 } 683 return buf.Bytes(), nil 684 } 685 686 func newItemInsideTar(out *tar.Writer, content []byte, header *tar.Header) error { 687 if err := out.WriteHeader(header); err != nil { 688 return fmt.Errorf("cannot write header of %s file to control.tar.gz: %w", header.Name, err) 689 } 690 if _, err := out.Write(content); err != nil { 691 return fmt.Errorf("cannot write %s file to control.tar.gz: %w", header.Name, err) 692 } 693 return nil 694 } 695 696 func newFileInsideTar(out *tar.Writer, name string, content []byte, modtime time.Time) error { 697 return newItemInsideTar(out, content, &tar.Header{ 698 Name: files.AsExplicitRelativePath(name), 699 Size: int64(len(content)), 700 Mode: 0o644, 701 ModTime: modtime, 702 Typeflag: tar.TypeReg, 703 Format: tar.FormatGNU, 704 }) 705 } 706 707 func newFilePathInsideTar(out *tar.Writer, path, dest string, mode int64, modtime time.Time) error { 708 content, err := os.ReadFile(path) 709 if err != nil { 710 return err 711 } 712 return newItemInsideTar(out, content, &tar.Header{ 713 Name: files.AsExplicitRelativePath(dest), 714 Size: int64(len(content)), 715 Mode: mode, 716 ModTime: modtime, 717 Typeflag: tar.TypeReg, 718 Format: tar.FormatGNU, 719 }) 720 } 721 722 func conffiles(info *nfpm.Info) ([]byte, bool) { 723 // nolint: prealloc 724 var confs []string 725 for _, file := range info.Contents { 726 switch file.Type { 727 case files.TypeConfig, files.TypeConfigNoReplace, files.TypeConfigMissingOK: 728 confs = append(confs, files.NormalizeAbsoluteFilePath(file.Destination)) 729 } 730 } 731 if len(confs) == 0 { 732 return nil, false 733 } 734 735 return []byte(strings.Join(confs, "\n") + "\n"), true 736 } 737 738 func createTriggers(info *nfpm.Info) []byte { 739 var buffer bytes.Buffer 740 741 // https://man7.org/linux/man-pages/man5/deb-triggers.5.html 742 triggerEntries := []struct { 743 Directive string 744 TriggerNames *[]string 745 }{ 746 {"interest", &info.Deb.Triggers.Interest}, 747 {"interest-await", &info.Deb.Triggers.InterestAwait}, 748 {"interest-noawait", &info.Deb.Triggers.InterestNoAwait}, 749 {"activate", &info.Deb.Triggers.Activate}, 750 {"activate-await", &info.Deb.Triggers.ActivateAwait}, 751 {"activate-noawait", &info.Deb.Triggers.ActivateNoAwait}, 752 } 753 754 for _, triggerEntry := range triggerEntries { 755 for _, triggerName := range *triggerEntry.TriggerNames { 756 fmt.Fprintf(&buffer, "%s %s\n", triggerEntry.Directive, triggerName) 757 } 758 } 759 760 return buffer.Bytes() 761 } 762 763 const controlTemplate = ` 764 {{- /* Mandatory fields */ -}} 765 Package: {{.Info.Name}} 766 Version: {{ if .Info.Epoch}}{{ .Info.Epoch }}:{{ end }}{{.Info.Version}} 767 {{- if .Info.Prerelease}}~{{ .Info.Prerelease }}{{- end }} 768 {{- if .Info.VersionMetadata}}+{{ .Info.VersionMetadata }}{{- end }} 769 {{- if .Info.Release}}-{{ .Info.Release }}{{- end }} 770 Section: {{.Info.Section}} 771 Priority: {{.Info.Priority}} 772 Architecture: {{ if ne .Info.Platform "linux"}}{{ .Info.Platform }}-{{ end }}{{.Info.Arch}} 773 {{- /* Optional fields */ -}} 774 {{- if .Info.License }} 775 License: {{.Info.License}} 776 {{- end }} 777 {{- if .Info.Maintainer}} 778 Maintainer: {{.Info.Maintainer}} 779 {{- end }} 780 Installed-Size: {{.InstalledSize}} 781 {{- with .Info.Replaces}} 782 Replaces: {{join .}} 783 {{- end }} 784 {{- with nonEmpty .Info.Provides}} 785 Provides: {{join .}} 786 {{- end }} 787 {{- with .Info.Deb.Predepends}} 788 Pre-Depends: {{join .}} 789 {{- end }} 790 {{- with .Info.Depends}} 791 Depends: {{join .}} 792 {{- end }} 793 {{- with .Info.Recommends}} 794 Recommends: {{join .}} 795 {{- end }} 796 {{- with .Info.Suggests}} 797 Suggests: {{join .}} 798 {{- end }} 799 {{- with .Info.Conflicts}} 800 Conflicts: {{join .}} 801 {{- end }} 802 {{- with .Info.Deb.Breaks}} 803 Breaks: {{join .}} 804 {{- end }} 805 {{- if .Info.Homepage}} 806 Homepage: {{.Info.Homepage}} 807 {{- end }} 808 {{- /* Mandatory fields */}} 809 Description: {{multiline .Info.Description}} 810 {{- range $key, $value := .Info.Deb.Fields }} 811 {{- if $value }} 812 {{$key}}: {{$value}} 813 {{- end }} 814 {{- end }} 815 ` 816 817 type controlData struct { 818 Info *nfpm.Info 819 InstalledSize int64 820 } 821 822 func writeControl(w io.Writer, data controlData) error { 823 tmpl := template.New("control") 824 tmpl.Funcs(template.FuncMap{ 825 "join": func(strs []string) string { 826 return strings.Trim(strings.Join(strs, ", "), " ") 827 }, 828 "multiline": func(strs string) string { 829 var b strings.Builder 830 s := bufio.NewScanner(strings.NewReader(strings.TrimSpace(strs))) 831 s.Scan() 832 b.Write(bytes.TrimSpace(s.Bytes())) 833 for s.Scan() { 834 b.WriteString("\n ") 835 l := bytes.TrimSpace(s.Bytes()) 836 if len(l) == 0 { 837 b.WriteByte('.') 838 } else { 839 b.Write(l) 840 } 841 } 842 return b.String() 843 }, 844 "nonEmpty": func(strs []string) []string { 845 var result []string 846 for _, s := range strs { 847 s := strings.TrimSpace(s) 848 if s == "" { 849 continue 850 } 851 result = append(result, s) 852 } 853 return result 854 }, 855 }) 856 return template.Must(tmpl.Parse(controlTemplate)).Execute(w, data) 857 } 858 859 func tarHeader(content *files.Content, preferredModTimes ...time.Time) (*tar.Header, error) { 860 const ( 861 ISUID = 0o4000 // Set uid 862 ISGID = 0o2000 // Set gid 863 ISVTX = 0o1000 // Save text (sticky bit) 864 ) 865 866 fm := content.Mode() 867 868 h := &tar.Header{ 869 Name: content.Name(), 870 ModTime: modtime.Get( 871 append(preferredModTimes, content.ModTime())...), 872 Mode: int64(fm & 0o7777), 873 Uname: content.FileInfo.Owner, 874 Gname: content.FileInfo.Group, 875 Format: tar.FormatGNU, 876 } 877 878 switch { 879 case content.IsDir() || fm&fs.ModeDir != 0: 880 h.Typeflag = tar.TypeDir 881 h.Name = files.AsExplicitRelativePath(content.Destination) 882 case content.Type == files.TypeSymlink || fm&fs.ModeSymlink != 0: 883 h.Typeflag = tar.TypeSymlink 884 h.Name = files.AsExplicitRelativePath(content.Destination) 885 h.Linkname = content.Source 886 case fm&fs.ModeDevice != 0: 887 if fm&fs.ModeCharDevice != 0 { 888 h.Typeflag = tar.TypeChar 889 } else { 890 h.Typeflag = tar.TypeBlock 891 } 892 case fm&fs.ModeNamedPipe != 0: 893 h.Typeflag = tar.TypeFifo 894 case fm&fs.ModeSocket != 0: 895 return nil, fmt.Errorf("archive/tar: sockets not supported") 896 default: 897 h.Typeflag = tar.TypeReg 898 h.Name = files.AsExplicitRelativePath(content.Destination) 899 h.Size = content.Size() 900 } 901 902 if fm&fs.ModeSetuid != 0 { 903 h.Mode |= ISUID 904 } 905 if fm&fs.ModeSetgid != 0 { 906 h.Mode |= ISGID 907 } 908 if fm&fs.ModeSticky != 0 { 909 h.Mode |= ISVTX 910 } 911 912 return h, nil 913 }