github.com/goreleaser/nfpm/v2@v2.44.0/rpm/rpm.go (about) 1 // Package rpm implements nfpm.Packager providing .rpm bindings using 2 // google/rpmpack. 3 package rpm 4 5 import ( 6 "bytes" 7 "fmt" 8 "io" 9 "os" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/google/rpmpack" 15 "github.com/goreleaser/chglog" 16 "github.com/goreleaser/nfpm/v2" 17 "github.com/goreleaser/nfpm/v2/files" 18 "github.com/goreleaser/nfpm/v2/internal/modtime" 19 "github.com/goreleaser/nfpm/v2/internal/sign" 20 ) 21 22 const ( 23 // https://github.com/rpm-software-management/rpm/blob/master/lib/rpmtag.h#L152 24 tagChangelogTime = 1080 25 // https://github.com/rpm-software-management/rpm/blob/master/lib/rpmtag.h#L153 26 tagChangelogName = 1081 27 // https://github.com/rpm-software-management/rpm/blob/master/lib/rpmtag.h#L154 28 tagChangelogText = 1082 29 // https://github.com/rpm-software-management/rpm/blob/master/include/rpm/rpmtag.h#L183 30 tagSourcePackage = 1106 31 32 // Symbolic link 33 tagLink = 0o120000 34 // Directory 35 tagDirectory = 0o40000 36 37 changelogNotesTemplate = ` 38 {{- range .Changes }}{{$note := splitList "\n" .Note}} 39 - {{ first $note }} 40 {{- range $i,$n := (rest $note) }}{{- if ne (trim $n) ""}} 41 {{$n}}{{end}} 42 {{- end}}{{- end}}` 43 ) 44 45 // nolint: gochecknoinits 46 func init() { 47 nfpm.RegisterPackager(formatRPM.String(), DefaultRPM) 48 nfpm.RegisterPackager(formatSRPM.String(), DefaultSRPM) 49 } 50 51 // DefaultRPM RPM packager. 52 // nolint: gochecknoglobals 53 var DefaultRPM = &RPM{formatRPM} 54 55 // DefaultRPM RPM packager. 56 // nolint: gochecknoglobals 57 var DefaultSRPM = &RPM{formatSRPM} 58 59 type format uint 60 61 const ( 62 formatRPM format = iota 63 formatSRPM 64 ) 65 66 // String implements fmt.Stringer. 67 func (f format) String() string { return [2]string{"rpm", "srpm"}[f] } 68 69 // RPM is a RPM packager implementation. 70 type RPM struct { 71 format format 72 } 73 74 // https://docs.fedoraproject.org/ro/Fedora_Draft_Documentation/0.1/html/RPM_Guide/ch01s03.html 75 // nolint: gochecknoglobals 76 var archToRPM = map[string]string{ 77 "all": "noarch", 78 "amd64": "x86_64", 79 "386": "i386", 80 "arm64": "aarch64", 81 "arm5": "armv5tel", 82 "arm6": "armv6hl", 83 "arm7": "armv7hl", 84 "mips64le": "mips64el", 85 "mipsle": "mipsel", 86 "mips": "mips", 87 // TODO: other arches 88 } 89 90 func setDefaults(info *nfpm.Info) *nfpm.Info { 91 if info.RPM.Arch != "" { 92 info.Arch = info.RPM.Arch 93 } else if arch, ok := archToRPM[info.Arch]; ok { 94 info.Arch = arch 95 } 96 97 info.Release = defaultTo(info.Release, "1") 98 99 return info 100 } 101 102 // ConventionalFileName returns a file name according 103 // to the conventions for RPM packages. See: 104 // http://ftp.rpm.org/max-rpm/ch-rpm-file-format.html 105 func (r *RPM) ConventionalFileName(info *nfpm.Info) string { 106 info = setDefaults(info) 107 108 // name-version-release.architecture.rpm 109 return fmt.Sprintf( 110 "%s-%s-%s.%s%s", 111 info.Name, 112 formatVersion(info), 113 defaultTo(info.Release, "1"), 114 info.Arch, 115 r.ConventionalExtension(), 116 ) 117 } 118 119 // ConventionalExtension returns the file name conventionally used for RPM packages 120 func (r *RPM) ConventionalExtension() string { 121 if r.format == formatSRPM { 122 return ".src.rpm" 123 } 124 return ".rpm" 125 } 126 127 // Package writes a new RPM package to the given writer using the given info. 128 func (r *RPM) Package(info *nfpm.Info, w io.Writer) (err error) { 129 var ( 130 meta *rpmpack.RPMMetaData 131 rpm *rpmpack.RPM 132 ) 133 info = setDefaults(info) 134 135 err = nfpm.PrepareForPackager(info, "rpm") 136 if err != nil { 137 return err 138 } 139 140 if meta, err = buildRPMMeta(info); err != nil { 141 return err 142 } 143 if rpm, err = rpmpack.NewRPM(*meta); err != nil { 144 return err 145 } 146 147 if r.format == formatSRPM { 148 rpm.AddCustomTag(tagSourcePackage, rpmpack.EntryUint32([]uint32{1})) 149 } 150 151 if info.RPM.Signature.KeyFile != "" { 152 rpm.SetPGPSigner(sign.PGPSignerWithKeyID( 153 info.RPM.Signature.KeyFile, 154 info.RPM.Signature.KeyPassphrase, 155 info.RPM.Signature.KeyID, 156 )) 157 } 158 if signFn := info.RPM.Signature.SignFn; signFn != nil { 159 rpm.SetPGPSigner(func(data []byte) ([]byte, error) { 160 return signFn(bytes.NewReader(data)) 161 }) 162 } 163 164 if err = createFilesInsideRPM(info, rpm); err != nil { 165 return err 166 } 167 168 if err = addScriptFiles(info, rpm); err != nil { 169 return err 170 } 171 172 if info.Changelog != "" { 173 if err = addChangeLog(info, rpm); err != nil { 174 return err 175 } 176 } 177 178 return rpm.Write(w) 179 } 180 181 func addChangeLog(info *nfpm.Info, rpm *rpmpack.RPM) error { 182 changelog, err := info.GetChangeLog() 183 if err != nil { 184 return fmt.Errorf("reading changelog: %w", err) 185 } 186 187 if len(changelog.Entries) == 0 { 188 // no nothing because creating empty tags 189 // would result in an invalid package 190 return nil 191 } 192 193 tpl, err := chglog.LoadTemplateData(changelogNotesTemplate) 194 if err != nil { 195 return fmt.Errorf("parsing RPM changelog template: %w", err) 196 } 197 198 changes := make([]string, len(changelog.Entries)) 199 titles := make([]string, len(changelog.Entries)) 200 times := make([]uint32, len(changelog.Entries)) 201 for idx, entry := range changelog.Entries { 202 var formattedNotes bytes.Buffer 203 204 err := tpl.Execute(&formattedNotes, entry) 205 if err != nil { 206 return fmt.Errorf("formatting changelog notes: %w", err) 207 } 208 209 changes[idx] = strings.TrimSpace(formattedNotes.String()) 210 times[idx] = uint32(entry.Date.Unix()) 211 titles[idx] = fmt.Sprintf("%s - %s", entry.Packager, entry.Semver) 212 } 213 214 rpm.AddCustomTag(tagChangelogTime, rpmpack.EntryUint32(times)) 215 rpm.AddCustomTag(tagChangelogName, rpmpack.EntryStringSlice(titles)) 216 rpm.AddCustomTag(tagChangelogText, rpmpack.EntryStringSlice(changes)) 217 218 return nil 219 } 220 221 //nolint:funlen 222 func buildRPMMeta(info *nfpm.Info) (*rpmpack.RPMMetaData, error) { 223 var ( 224 err error 225 epoch uint64 226 provides, 227 depends, 228 recommends, 229 replaces, 230 suggests, 231 conflicts rpmpack.Relations 232 ) 233 if info.RPM.Compression == "" { 234 info.RPM.Compression = "gzip:-1" 235 } 236 237 if info.Epoch == "" { 238 epoch = uint64(rpmpack.NoEpoch) 239 } else { 240 if epoch, err = strconv.ParseUint(info.Epoch, 10, 32); err != nil { 241 return nil, err 242 } 243 } 244 if provides, err = toRelation(info.Provides); err != nil { 245 return nil, err 246 } 247 if depends, err = toRelation(info.Depends); err != nil { 248 return nil, err 249 } 250 if recommends, err = toRelation(info.Recommends); err != nil { 251 return nil, err 252 } 253 if replaces, err = toRelation(info.Replaces); err != nil { 254 return nil, err 255 } 256 if suggests, err = toRelation(info.Suggests); err != nil { 257 return nil, err 258 } 259 if conflicts, err = toRelation(info.Conflicts); err != nil { 260 return nil, err 261 } 262 263 hostname := info.RPM.BuildHost 264 if hostname == "" { 265 hostname, err = os.Hostname() 266 if err != nil { 267 return nil, err 268 } 269 } 270 271 return &rpmpack.RPMMetaData{ 272 Name: info.Name, 273 Summary: defaultTo(info.RPM.Summary, strings.Split(info.Description, "\n")[0]), 274 Description: info.Description, 275 Version: formatVersion(info), 276 Release: defaultTo(info.Release, "1"), 277 Epoch: uint32(epoch), 278 Arch: info.Arch, 279 OS: info.Platform, 280 Licence: info.License, 281 URL: info.Homepage, 282 Vendor: info.Vendor, 283 Packager: defaultTo(info.RPM.Packager, info.Maintainer), 284 Prefixes: info.RPM.Prefixes, 285 Group: info.RPM.Group, 286 Provides: provides, 287 Recommends: recommends, 288 Requires: depends, 289 Obsoletes: replaces, 290 Suggests: suggests, 291 Conflicts: conflicts, 292 Compressor: info.RPM.Compression, 293 BuildTime: modtime.Get(info.MTime), 294 BuildHost: hostname, 295 }, nil 296 } 297 298 func formatVersion(info *nfpm.Info) string { 299 version := info.Version 300 301 if info.Prerelease != "" { 302 version += "~" + strings.ReplaceAll(info.Prerelease, "-", "_") 303 } 304 305 if info.VersionMetadata != "" { 306 version += "+" + info.VersionMetadata 307 } 308 309 return version 310 } 311 312 func defaultTo(in, def string) string { 313 if in == "" { 314 return def 315 } 316 return in 317 } 318 319 func toRelation(items []string) (rpmpack.Relations, error) { 320 relations := make(rpmpack.Relations, 0) 321 for idx := range items { 322 if err := relations.Set(items[idx]); err != nil { 323 return nil, err 324 } 325 } 326 327 return relations, nil 328 } 329 330 func addScriptFiles(info *nfpm.Info, rpm *rpmpack.RPM) error { 331 if info.RPM.Scripts.PreTrans != "" { 332 data, err := os.ReadFile(info.RPM.Scripts.PreTrans) 333 if err != nil { 334 return err 335 } 336 rpm.AddPretrans(string(data)) 337 } 338 if info.Scripts.PreInstall != "" { 339 data, err := os.ReadFile(info.Scripts.PreInstall) 340 if err != nil { 341 return err 342 } 343 rpm.AddPrein(string(data)) 344 } 345 346 if info.Scripts.PreRemove != "" { 347 data, err := os.ReadFile(info.Scripts.PreRemove) 348 if err != nil { 349 return err 350 } 351 rpm.AddPreun(string(data)) 352 } 353 354 if info.Scripts.PostInstall != "" { 355 data, err := os.ReadFile(info.Scripts.PostInstall) 356 if err != nil { 357 return err 358 } 359 rpm.AddPostin(string(data)) 360 } 361 362 if info.Scripts.PostRemove != "" { 363 data, err := os.ReadFile(info.Scripts.PostRemove) 364 if err != nil { 365 return err 366 } 367 rpm.AddPostun(string(data)) 368 } 369 370 if info.RPM.Scripts.PostTrans != "" { 371 data, err := os.ReadFile(info.RPM.Scripts.PostTrans) 372 if err != nil { 373 return err 374 } 375 rpm.AddPosttrans(string(data)) 376 } 377 378 if info.RPM.Scripts.Verify != "" { 379 data, err := os.ReadFile(info.RPM.Scripts.Verify) 380 if err != nil { 381 return err 382 } 383 rpm.AddVerifyScript(string(data)) 384 } 385 386 return nil 387 } 388 389 // TODO: pass mtime down in all content types 390 func createFilesInsideRPM(info *nfpm.Info, rpm *rpmpack.RPM) (err error) { 391 mtime := modtime.Get(info.MTime) 392 for _, content := range info.Contents { 393 if content.Packager != "" && content.Packager != "rpm" { 394 continue 395 } 396 397 var file *rpmpack.RPMFile 398 399 switch content.Type { 400 case files.TypeConfig: 401 file, err = asRPMFile(content, rpmpack.ConfigFile) 402 case files.TypeConfigNoReplace: 403 file, err = asRPMFile(content, rpmpack.ConfigFile|rpmpack.NoReplaceFile) 404 case files.TypeConfigMissingOK: 405 file, err = asRPMFile(content, rpmpack.ConfigFile|rpmpack.MissingOkFile) 406 case files.TypeRPMGhost: 407 if content.FileInfo.Mode == 0 { 408 content.FileInfo.Mode = os.FileMode(0o644) 409 } 410 411 file, err = asRPMFile(content, rpmpack.GhostFile) 412 case files.TypeRPMDoc: 413 file, err = asRPMFile(content, rpmpack.DocFile) 414 case files.TypeRPMLicence, files.TypeRPMLicense: 415 file, err = asRPMFile(content, rpmpack.LicenceFile) 416 case files.TypeRPMReadme: 417 file, err = asRPMFile(content, rpmpack.ReadmeFile) 418 case files.TypeSymlink: 419 file = asRPMSymlink(content) 420 case files.TypeDir: 421 file = asRPMDirectory(content, mtime) 422 case files.TypeImplicitDir: 423 // we don't need to add imlicit directories to RPMs 424 continue 425 default: 426 file, err = asRPMFile(content, rpmpack.GenericFile) 427 } 428 429 if err != nil { 430 return err 431 } 432 433 // clean assures that even folders do not have a trailing slash 434 file.Name = files.ToNixPath(file.Name) 435 rpm.AddFile(*file) 436 437 } 438 439 return nil 440 } 441 442 func asRPMDirectory(content *files.Content, mtime time.Time) *rpmpack.RPMFile { 443 return &rpmpack.RPMFile{ 444 Name: content.Destination, 445 Mode: uint(content.Mode()) | tagDirectory, 446 MTime: uint32(mtime.Unix()), 447 Owner: content.FileInfo.Owner, 448 Group: content.FileInfo.Group, 449 } 450 } 451 452 func asRPMSymlink(content *files.Content) *rpmpack.RPMFile { 453 return &rpmpack.RPMFile{ 454 Name: content.Destination, 455 Body: []byte(content.Source), 456 Mode: uint(tagLink), 457 MTime: uint32(content.FileInfo.MTime.Unix()), 458 Owner: content.FileInfo.Owner, 459 Group: content.FileInfo.Group, 460 } 461 } 462 463 func asRPMFile(content *files.Content, fileType rpmpack.FileType) (*rpmpack.RPMFile, error) { 464 data, err := os.ReadFile(content.Source) 465 if err != nil && content.Type != files.TypeRPMGhost { 466 return nil, err 467 } 468 469 return &rpmpack.RPMFile{ 470 Name: content.Destination, 471 Body: data, 472 Mode: uint(content.FileInfo.Mode), 473 MTime: uint32(content.FileInfo.MTime.Unix()), 474 Owner: content.FileInfo.Owner, 475 Group: content.FileInfo.Group, 476 Type: fileType, 477 }, nil 478 }