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  }