// Package nfpm provides ways to package programs in some linux packaging // formats. package nfpm import ( "errors" "fmt" "io" "io/fs" "os" "strings" "sync" "github.com/AlekSi/pointer" "github.com/Masterminds/semver/v3" "github.com/goreleaser/chglog" "github.com/goreleaser/nfpm/v2/files" "github.com/imdario/mergo" "gopkg.in/yaml.v3" ) // nolint: gochecknoglobals var ( packagers = map[string]Packager{} lock sync.Mutex ) // RegisterPackager a new packager for the given format. func RegisterPackager(format string, p Packager) { lock.Lock() defer lock.Unlock() packagers[format] = p } // ClearPackagers clear all registered packagers, used for testing. func ClearPackagers() { lock.Lock() defer lock.Unlock() packagers = map[string]Packager{} } // ErrNoPackager happens when no packager is registered for the given format. type ErrNoPackager struct { format string } func (e ErrNoPackager) Error() string { return fmt.Sprintf("no packager registered for the format %s", e.format) } // Get a packager for the given format. func Get(format string) (Packager, error) { p, ok := packagers[format] if !ok { return nil, ErrNoPackager{format} } return p, nil } // Parse decodes YAML data from an io.Reader into a configuration struct. func Parse(in io.Reader) (config Config, err error) { return ParseWithEnvMapping(in, os.Getenv) } // ParseWithEnvMapping decodes YAML data from an io.Reader into a configuration struct. func ParseWithEnvMapping(in io.Reader, mapping func(string) string) (config Config, err error) { dec := yaml.NewDecoder(in) dec.KnownFields(true) if err = dec.Decode(&config); err != nil { return } config.envMappingFunc = mapping if config.envMappingFunc == nil { config.envMappingFunc = func(s string) string { return s } } config.expandEnvVars() WithDefaults(&config.Info) return config, nil } // ParseFile decodes YAML data from a file path into a configuration struct. func ParseFile(path string) (config Config, err error) { if path == "-" { return ParseWithEnvMapping(os.Stdin, os.Getenv) } return ParseFileWithEnvMapping(path, os.Getenv) } // ParseFileWithEnvMapping decodes YAML data from a file path into a configuration struct. func ParseFileWithEnvMapping(path string, mapping func(string) string) (config Config, err error) { var file *os.File file, err = os.Open(path) //nolint:gosec if err != nil { return } defer file.Close() // nolint: errcheck,gosec return ParseWithEnvMapping(file, mapping) } // Packager represents any packager implementation. type Packager interface { Package(info *Info, w io.Writer) error ConventionalFileName(info *Info) string } type PackagerWithExtension interface { Packager ConventionalExtension() string } // Config contains the top level configuration for packages. type Config struct { Info `yaml:",inline" json:",inline"` Overrides map[string]*Overridables `yaml:"overrides,omitempty" json:"overrides,omitempty" jsonschema:"title=overrides,description=override some fields when packaging with a specific packager,enum=apk,enum=deb,enum=rpm"` envMappingFunc func(string) string } // Get returns the Info struct for the given packager format. Overrides // for the given format are merged into the final struct. func (c *Config) Get(format string) (info *Info, err error) { info = &Info{} // make a deep copy of info if err = mergo.Merge(info, c.Info); err != nil { return nil, fmt.Errorf("failed to merge config into info: %w", err) } override, ok := c.Overrides[format] if !ok { // no overrides return info, nil } if err = mergo.Merge(&info.Overridables, override, mergo.WithOverride); err != nil { return nil, fmt.Errorf("failed to merge overrides into info: %w", err) } var contents []*files.Content for _, f := range info.Contents { if f.Packager == format || f.Packager == "" { contents = append(contents, f) } } info.Contents = contents return info, nil } // Validate ensures that the config is well typed. func (c *Config) Validate() error { if err := Validate(&c.Info); err != nil { return err } for format := range c.Overrides { if _, err := Get(format); err != nil { return err } } return nil } func (c *Config) expandEnvVarsStringSlice(items []string) []string { for i, dep := range items { val := strings.TrimSpace(os.Expand(dep, c.envMappingFunc)) items[i] = val } for i := 0; i < len(items); i++ { if items[i] == "" { items = append(items[:i], items[i+1:]...) i-- // Since we just deleted items[i], we must redo that index } } return items } func (c *Config) expandEnvVars() { // Version related fields c.Info.Release = os.Expand(c.Info.Release, c.envMappingFunc) c.Info.Version = os.Expand(c.Info.Version, c.envMappingFunc) c.Info.Prerelease = os.Expand(c.Info.Prerelease, c.envMappingFunc) c.Info.Platform = os.Expand(c.Info.Platform, c.envMappingFunc) c.Info.Arch = os.Expand(c.Info.Arch, c.envMappingFunc) for or := range c.Overrides { c.Overrides[or].Conflicts = c.expandEnvVarsStringSlice(c.Overrides[or].Conflicts) c.Overrides[or].Depends = c.expandEnvVarsStringSlice(c.Overrides[or].Depends) c.Overrides[or].Replaces = c.expandEnvVarsStringSlice(c.Overrides[or].Replaces) c.Overrides[or].Recommends = c.expandEnvVarsStringSlice(c.Overrides[or].Recommends) c.Overrides[or].Provides = c.expandEnvVarsStringSlice(c.Overrides[or].Provides) c.Overrides[or].Suggests = c.expandEnvVarsStringSlice(c.Overrides[or].Suggests) } c.Info.Conflicts = c.expandEnvVarsStringSlice(c.Info.Conflicts) c.Info.Depends = c.expandEnvVarsStringSlice(c.Info.Depends) c.Info.Replaces = c.expandEnvVarsStringSlice(c.Info.Replaces) c.Info.Recommends = c.expandEnvVarsStringSlice(c.Info.Recommends) c.Info.Provides = c.expandEnvVarsStringSlice(c.Info.Provides) c.Info.Suggests = c.expandEnvVarsStringSlice(c.Info.Suggests) // Maintainer and vendor fields c.Info.Name = os.Expand(c.Info.Name, c.envMappingFunc) c.Info.Maintainer = os.Expand(c.Info.Maintainer, c.envMappingFunc) c.Info.Vendor = os.Expand(c.Info.Vendor, c.envMappingFunc) // Package signing related fields c.Info.Deb.Signature.KeyFile = os.Expand(c.Deb.Signature.KeyFile, c.envMappingFunc) c.Info.RPM.Signature.KeyFile = os.Expand(c.RPM.Signature.KeyFile, c.envMappingFunc) c.Info.APK.Signature.KeyFile = os.Expand(c.APK.Signature.KeyFile, c.envMappingFunc) c.Info.Deb.Signature.KeyID = pointer.ToString(os.Expand(pointer.GetString(c.Deb.Signature.KeyID), c.envMappingFunc)) c.Info.RPM.Signature.KeyID = pointer.ToString(os.Expand(pointer.GetString(c.RPM.Signature.KeyID), c.envMappingFunc)) c.Info.APK.Signature.KeyID = pointer.ToString(os.Expand(pointer.GetString(c.APK.Signature.KeyID), c.envMappingFunc)) // Package signing passphrase generalPassphrase := os.Expand("$NFPM_PASSPHRASE", c.envMappingFunc) c.Info.Deb.Signature.KeyPassphrase = generalPassphrase c.Info.RPM.Signature.KeyPassphrase = generalPassphrase c.Info.APK.Signature.KeyPassphrase = generalPassphrase debPassphrase := os.Expand("$NFPM_DEB_PASSPHRASE", c.envMappingFunc) if debPassphrase != "" { c.Info.Deb.Signature.KeyPassphrase = debPassphrase } rpmPassphrase := os.Expand("$NFPM_RPM_PASSPHRASE", c.envMappingFunc) if rpmPassphrase != "" { c.Info.RPM.Signature.KeyPassphrase = rpmPassphrase } apkPassphrase := os.Expand("$NFPM_APK_PASSPHRASE", c.envMappingFunc) if apkPassphrase != "" { c.Info.APK.Signature.KeyPassphrase = apkPassphrase } // RPM specific c.Info.RPM.Packager = os.Expand(c.RPM.Packager, c.envMappingFunc) } // Info contains information about a single package. type Info struct { Overridables `yaml:",inline" json:",inline"` Name string `yaml:"name" json:"name" jsonschema:"title=package name"` Arch string `yaml:"arch" json:"arch" jsonschema:"title=target architecture,example=amd64"` Platform string `yaml:"platform,omitempty" json:"platform,omitempty" jsonschema:"title=target platform,example=linux,default=linux"` Epoch string `yaml:"epoch,omitempty" json:"epoch,omitempty" jsonschema:"title=version epoch,example=2,default=extracted from version"` Version string `yaml:"version" json:"version" jsonschema:"title=version,example=v1.0.2,example=2.0.1"` VersionSchema string `yaml:"version_schema,omitempty" json:"version_schema,omitempty" jsonschema:"title=version schema,enum=semver,enum=none,default=semver"` Release string `yaml:"release,omitempty" json:"release,omitempty" jsonschema:"title=version release,example=1"` Prerelease string `yaml:"prerelease,omitempty" json:"prerelease,omitempty" jsonschema:"title=version prerelease,default=extracted from version"` VersionMetadata string `yaml:"version_metadata,omitempty" json:"version_metadata,omitempty" jsonschema:"title=version metadata,example=git"` Section string `yaml:"section,omitempty" json:"section,omitempty" jsonschema:"title=package section,example=default"` Priority string `yaml:"priority,omitempty" json:"priority,omitempty" jsonschema:"title=package priority,example=extra"` Maintainer string `yaml:"maintainer,omitempty" json:"maintainer,omitempty" jsonschema:"title=package maintainer,example=me@example.com"` Description string `yaml:"description,omitempty" json:"description,omitempty" jsonschema:"title=package description"` Vendor string `yaml:"vendor,omitempty" json:"vendor,omitempty" jsonschema:"title=package vendor,example=MyCorp"` Homepage string `yaml:"homepage,omitempty" json:"homepage,omitempty" jsonschema:"title=package homepage,example=https://example.com"` License string `yaml:"license,omitempty" json:"license,omitempty" jsonschema:"title=package license,example=MIT"` Changelog string `yaml:"changelog,omitempty" json:"changelog,omitempty" jsonschema:"title=package changelog,example=changelog.yaml,description=see https://github.com/goreleaser/chglog for more details"` DisableGlobbing bool `yaml:"disable_globbing,omitempty" json:"disable_globbing,omitempty" jsonschema:"title=whether to disable file globbing,default=false"` Target string `yaml:"-" json:"-"` } func (i *Info) Validate() error { return Validate(i) } // GetChangeLog parses the provided changelog file. func (i *Info) GetChangeLog() (log *chglog.PackageChangeLog, err error) { // if the file does not exist chglog.Parse will just silently // create an empty changelog but we should notify the user instead if _, err = os.Stat(i.Changelog); errors.Is(err, fs.ErrNotExist) { return nil, err } entries, err := chglog.Parse(i.Changelog) if err != nil { return nil, err } return &chglog.PackageChangeLog{ Name: i.Name, Entries: entries, }, nil } func (i *Info) parseSemver() { // parse the version as a semver so we can properly split the parts // and support proper ordering for both rpm and deb if v, err := semver.NewVersion(i.Version); err == nil { i.Version = fmt.Sprintf("%d.%d.%d", v.Major(), v.Minor(), v.Patch()) if i.Prerelease == "" { i.Prerelease = v.Prerelease() } if i.VersionMetadata == "" { i.VersionMetadata = v.Metadata() } } } // Overridables contain the field which are overridable in a package. type Overridables struct { Replaces []string `yaml:"replaces,omitempty" json:"replaces,omitempty" jsonschema:"title=replaces directive,example=nfpm"` Provides []string `yaml:"provides,omitempty" json:"provides,omitempty" jsonschema:"title=provides directive,example=nfpm"` Depends []string `yaml:"depends,omitempty" json:"depends,omitempty" jsonschema:"title=depends directive,example=nfpm"` Recommends []string `yaml:"recommends,omitempty" json:"recommends,omitempty" jsonschema:"title=recommends directive,example=nfpm"` Suggests []string `yaml:"suggests,omitempty" json:"suggests,omitempty" jsonschema:"title=suggests directive,example=nfpm"` Conflicts []string `yaml:"conflicts,omitempty" json:"conflicts,omitempty" jsonschema:"title=conflicts directive,example=nfpm"` Contents files.Contents `yaml:"contents,omitempty" json:"contents,omitempty" jsonschema:"title=files to add to the package"` Umask os.FileMode `yaml:"umask,omitempty" json:"umask,omitempty" jsonschema:"title=umask for file contents,example=002"` Scripts Scripts `yaml:"scripts,omitempty" json:"scripts,omitempty" jsonschema:"title=scripts to execute"` RPM RPM `yaml:"rpm,omitempty" json:"rpm,omitempty" jsonschema:"title=rpm-specific settings"` Deb Deb `yaml:"deb,omitempty" json:"deb,omitempty" jsonschema:"title=deb-specific settings"` APK APK `yaml:"apk,omitempty" json:"apk,omitempty" jsonschema:"title=apk-specific settings"` ArchLinux ArchLinux `yaml:"archlinux,omitempty" json:"archlinux,omitempty" jsonschema:"title=archlinux-specific settings"` } type ArchLinux struct { Pkgbase string `yaml:"pkgbase,omitempty" json:"pkgbase,omitempty" jsonschema:"title=explicitly specify the name used to refer to a split package, defaults to name"` Arch string `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"title=architecture in archlinux nomenclature"` Packager string `yaml:"packager,omitempty" json:"packager,omitempty" jsonschema:"title=organization that packaged the software"` Scripts ArchLinuxScripts `yaml:"scripts,omitempty" json:"scripts,omitempty" jsonschema:"title=archlinux-specific scripts"` } type ArchLinuxScripts struct { PreUpgrade string `yaml:"preupgrade,omitempty" json:"preupgrade,omitempty" jsonschema:"title=preupgrade script"` PostUpgrade string `yaml:"postupgrade,omitempty" json:"postupgrade,omitempty" jsonschema:"title=postupgrade script"` } // RPM is custom configs that are only available on RPM packages. type RPM struct { Arch string `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"title=architecture in rpm nomenclature"` Scripts RPMScripts `yaml:"scripts,omitempty" json:"scripts,omitempty" jsonschema:"title=rpm-specific scripts"` Group string `yaml:"group,omitempty" json:"group,omitempty" jsonschema:"title=package group,example=Unspecified"` Summary string `yaml:"summary,omitempty" json:"summary,omitempty" jsonschema:"title=package summary"` Compression string `yaml:"compression,omitempty" json:"compression,omitempty" jsonschema:"title=compression algorithm to be used,enum=gzip,enum=lzma,enum=xz,default=gzip:-1"` Signature RPMSignature `yaml:"signature,omitempty" json:"signature,omitempty" jsonschema:"title=rpm signature"` Packager string `yaml:"packager,omitempty" json:"packager,omitempty" jsonschema:"title=organization that actually packaged the software"` } // RPMScripts represents scripts only available on RPM packages. type RPMScripts struct { PreTrans string `yaml:"pretrans,omitempty" json:"pretrans,omitempty" jsonschema:"title=pretrans script"` PostTrans string `yaml:"posttrans,omitempty" json:"posttrans,omitempty" jsonschema:"title=posttrans script"` } type PackageSignature struct { // PGP secret key, can be ASCII-armored KeyFile string `yaml:"key_file,omitempty" json:"key_file,omitempty" jsonschema:"title=key file,example=key.gpg"` KeyID *string `yaml:"key_id,omitempty" json:"key_id,omitempty" jsonschema:"title=key id,example=bc8acdd415bd80b3"` KeyPassphrase string `yaml:"-" json:"-"` // populated from environment variable } type RPMSignature struct { PackageSignature `yaml:",inline" json:",inline"` } type APK struct { Arch string `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"title=architecture in apk nomenclature"` Signature APKSignature `yaml:"signature,omitempty" json:"signature,omitempty" jsonschema:"title=apk signature"` Scripts APKScripts `yaml:"scripts,omitempty" json:"scripts,omitempty" jsonschema:"title=apk scripts"` } type APKSignature struct { PackageSignature `yaml:",inline" json:",inline"` // defaults to .rsa.pub KeyName string `yaml:"key_name,omitempty" json:"key_name,omitempty" jsonschema:"title=key name,example=origin,default=maintainer_email.rsa.pub"` } type APKScripts struct { PreUpgrade string `yaml:"preupgrade,omitempty" json:"preupgrade,omitempty" jsonschema:"title=pre upgrade script"` PostUpgrade string `yaml:"postupgrade,omitempty" json:"postupgrade,omitempty" jsonschema:"title=post upgrade script"` } // Deb is custom configs that are only available on deb packages. type Deb struct { Arch string `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"title=architecture in deb nomenclature"` Scripts DebScripts `yaml:"scripts,omitempty" json:"scripts,omitempty" jsonschema:"title=scripts"` Triggers DebTriggers `yaml:"triggers,omitempty" json:"triggers,omitempty" jsonschema:"title=triggers"` Breaks []string `yaml:"breaks,omitempty" json:"breaks,omitempty" jsonschema:"title=breaks"` Signature DebSignature `yaml:"signature,omitempty" json:"signature,omitempty" jsonschema:"title=signature"` Compression string `yaml:"compression,omitempty" json:"compression,omitempty" jsonschema:"title=compression algorithm to be used,enum=gzip,enum=xz,enum=none,default=gzip"` Fields map[string]string `yaml:"fields,omitempty" json:"fields,omitempty" jsonschema:"title=fields"` } type DebSignature struct { PackageSignature `yaml:",inline" json:",inline"` // debsign, or dpkg-sig (defaults to debsign) Method string `yaml:"method,omitempty" json:"method,omitempty" jsonschema:"title=method role,enum=debsign,enum=dpkg-sig,default=debsign"` // origin, maint or archive (defaults to origin) Type string `yaml:"type,omitempty" json:"type,omitempty" jsonschema:"title=signer role,enum=origin,enum=maint,enum=archive,default=origin"` Signer string `yaml:"signer,omitempty" json:"signer,omitempty" jsonschema:"title=signer"` } // DebTriggers contains triggers only available for deb packages. // https://wiki.debian.org/DpkgTriggers // https://man7.org/linux/man-pages/man5/deb-triggers.5.html type DebTriggers struct { Interest []string `yaml:"interest,omitempty" json:"interest,omitempty" jsonschema:"title=interest"` InterestAwait []string `yaml:"interest_await,omitempty" json:"interest_await,omitempty" jsonschema:"title=interest await"` InterestNoAwait []string `yaml:"interest_noawait,omitempty" json:"interest_noawait,omitempty" jsonschema:"title=interest noawait"` Activate []string `yaml:"activate,omitempty" json:"activate,omitempty" jsonschema:"title=activate"` ActivateAwait []string `yaml:"activate_await,omitempty" json:"activate_await,omitempty" jsonschema:"title=activate await"` ActivateNoAwait []string `yaml:"activate_noawait,omitempty" json:"activate_noawait,omitempty" jsonschema:"title=activate noawait"` } // DebScripts is scripts only available on deb packages. type DebScripts struct { Rules string `yaml:"rules,omitempty" json:"rules,omitempty" jsonschema:"title=rules"` Templates string `yaml:"templates,omitempty" json:"templates,omitempty" jsonschema:"title=templates"` Config string `yaml:"config,omitempty" json:"config,omitempty" jsonschema:"title=config"` } // Scripts contains information about maintainer scripts for packages. type Scripts struct { PreInstall string `yaml:"preinstall,omitempty" json:"preinstall,omitempty" jsonschema:"title=pre install"` PostInstall string `yaml:"postinstall,omitempty" json:"postinstall,omitempty" jsonschema:"title=post install"` PreRemove string `yaml:"preremove,omitempty" json:"preremove,omitempty" jsonschema:"title=pre remove"` PostRemove string `yaml:"postremove,omitempty" json:"postremove,omitempty" jsonschema:"title=post remove"` } // ErrFieldEmpty happens when some required field is empty. type ErrFieldEmpty struct { field string } func (e ErrFieldEmpty) Error() string { return fmt.Sprintf("package %s must be provided", e.field) } // PrepareForPackager validates the configuration for the given packager and // prepares the contents for said packager. func PrepareForPackager(info *Info, packager string) (err error) { if info.Name == "" { return ErrFieldEmpty{"name"} } if info.Arch == "" && ((packager == "deb" && info.Deb.Arch == "") || (packager == "rpm" && info.RPM.Arch == "") || (packager == "apk" && info.APK.Arch == "")) { return ErrFieldEmpty{"arch"} } if info.Version == "" { return ErrFieldEmpty{"version"} } info.Contents, err = files.PrepareForPackager(info.Contents, info.Umask, packager, info.DisableGlobbing) return err } // Validate the given Info and returns an error if it is invalid. Validate will // no change the info's contents. func Validate(info *Info) (err error) { if info.Name == "" { return ErrFieldEmpty{"name"} } if info.Arch == "" && (info.Deb.Arch == "" || info.RPM.Arch == "" || info.APK.Arch == "") { return ErrFieldEmpty{"arch"} } if info.Version == "" { return ErrFieldEmpty{"version"} } for packager := range packagers { _, err := files.PrepareForPackager(info.Contents, info.Umask, packager, info.DisableGlobbing) if err != nil { return err } } return nil } // WithDefaults set some sane defaults into the given Info. func WithDefaults(info *Info) *Info { if info.Platform == "" { info.Platform = "linux" } if info.Description == "" { info.Description = "no description given" } if info.Arch == "" { info.Arch = "amd64" } if info.Version == "" { info.Version = "v0.0.0-rc0" } if info.Umask == 0 { info.Umask = 0o02 } switch info.VersionSchema { case "none": // No change to the version or prerelease info set in the YAML file break case "semver": fallthrough default: info.parseSemver() } return info } // ErrSigningFailure is returned whenever something went wrong during // the package signing process. The underlying error can be unwrapped // and could be crypto-related or something that occurred while adding // the signature to the package. type ErrSigningFailure struct { Err error } func (s *ErrSigningFailure) Error() string { return fmt.Sprintf("signing error: %v", s.Err) } func (s *ErrSigningFailure) Unwarp() error { return s.Err }