mirror of
https://github.com/goreleaser/nfpm
synced 2025-04-11 20:23:38 +02:00
575 lines
16 KiB
Go
575 lines
16 KiB
Go
package files
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/goreleaser/nfpm/v2/internal/glob"
|
|
)
|
|
|
|
const (
|
|
// TypeFile is the type of a regular file. This is also the type that is
|
|
// implied when no type is specified.
|
|
TypeFile = "file"
|
|
// TypeDir is the type of a directory that is explicitly added in order to
|
|
// declare ownership or non-standard permission.
|
|
TypeDir = "dir"
|
|
/// TypeImplicitDir is the type of a directory that is implicitly added as a
|
|
//parent of a file.
|
|
TypeImplicitDir = "implicit dir"
|
|
// TypeTree is the type of a whole directory tree structure.
|
|
TypeTree = "tree"
|
|
// TypeSymlink is the type of a symlink that is created at the destination
|
|
// path and points to the source path.
|
|
TypeSymlink = "symlink"
|
|
// TypeConfig is the type of a configuration file that may be changed by the
|
|
// user of the package.
|
|
TypeConfig = "config"
|
|
// TypeConfigNoReplace is like TypeConfig with an added noreplace directive
|
|
// that is respected by RPM-based distributions.
|
|
// For all other package formats it is handled exactly like TypeConfig.
|
|
TypeConfigNoReplace = "config|noreplace"
|
|
// TypeConfigMissingOK is like TypeConfig with an added missingok directive
|
|
// that is respected by RPM-based distributions.
|
|
// For all other package formats it is handled exactly like TypeConfig.
|
|
TypeConfigMissingOK = "config|missingok"
|
|
// TypeGhost is the type of an RPM ghost file which is ignored by other packagers.
|
|
TypeRPMGhost = "ghost"
|
|
// TypeRPMDoc is the type of an RPM doc file which is ignored by other packagers.
|
|
TypeRPMDoc = "doc"
|
|
// TypeRPMLicence is the type of an RPM licence file which is ignored by other packagers.
|
|
TypeRPMLicence = "licence"
|
|
// TypeRPMLicense a different spelling of TypeRPMLicence.
|
|
TypeRPMLicense = "license"
|
|
// TypeRPMReadme is the type of an RPM readme file which is ignored by other packagers.
|
|
TypeRPMReadme = "readme"
|
|
// TypeDebChangelog is the type of a Debian changelog archive file which is
|
|
// ignored by other packagers. This type should never be set for a content
|
|
// entry as it is automatically added when a changelog is configred.
|
|
TypeDebChangelog = "debian changelog"
|
|
)
|
|
|
|
// Content describes the source and destination
|
|
// of one file to copy into a package.
|
|
type Content struct {
|
|
Source string `yaml:"src,omitempty" json:"src,omitempty"`
|
|
Destination string `yaml:"dst" json:"dst"`
|
|
Type string `yaml:"type,omitempty" json:"type,omitempty" jsonschema:"enum=symlink,enum=ghost,enum=config,enum=config|noreplace,enum=dir,enum=tree,enum=,default="`
|
|
Packager string `yaml:"packager,omitempty" json:"packager,omitempty"`
|
|
FileInfo *ContentFileInfo `yaml:"file_info,omitempty" json:"file_info,omitempty"`
|
|
Expand bool `yaml:"expand,omitempty" json:"expand,omitempty"`
|
|
}
|
|
|
|
type ContentFileInfo struct {
|
|
Owner string `yaml:"owner,omitempty" json:"owner,omitempty"`
|
|
Group string `yaml:"group,omitempty" json:"group,omitempty"`
|
|
Mode os.FileMode `yaml:"mode,omitempty" json:"mode,omitempty"`
|
|
MTime time.Time `yaml:"mtime,omitempty" json:"mtime,omitempty"`
|
|
Size int64 `yaml:"-" json:"-"`
|
|
}
|
|
|
|
// Contents list of Content to process.
|
|
type Contents []*Content
|
|
|
|
func (c Contents) Len() int {
|
|
return len(c)
|
|
}
|
|
|
|
func (c Contents) Swap(i, j int) {
|
|
c[i], c[j] = c[j], c[i]
|
|
}
|
|
|
|
func (c Contents) Less(i, j int) bool {
|
|
a, b := c[i], c[j]
|
|
|
|
if a.Destination != b.Destination {
|
|
return a.Destination < b.Destination
|
|
}
|
|
|
|
if a.Type != b.Type {
|
|
return a.Type < b.Type
|
|
}
|
|
|
|
return a.Packager < b.Packager
|
|
}
|
|
|
|
func (c Contents) ContainsDestination(dst string) bool {
|
|
for _, content := range c {
|
|
if strings.TrimRight(content.Destination, "/") == strings.TrimRight(dst, "/") {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (c *Content) WithFileInfoDefaults(umask fs.FileMode, mtime time.Time) *Content {
|
|
cc := &Content{
|
|
Source: c.Source,
|
|
Destination: c.Destination,
|
|
Type: c.Type,
|
|
Packager: c.Packager,
|
|
FileInfo: c.FileInfo,
|
|
}
|
|
if cc.Type == "" {
|
|
cc.Type = TypeFile
|
|
}
|
|
if cc.FileInfo == nil {
|
|
cc.FileInfo = &ContentFileInfo{}
|
|
}
|
|
if cc.FileInfo.Owner == "" {
|
|
cc.FileInfo.Owner = "root"
|
|
}
|
|
if cc.FileInfo.Group == "" {
|
|
cc.FileInfo.Group = "root"
|
|
}
|
|
if (cc.Type == TypeDir || cc.Type == TypeImplicitDir) && cc.FileInfo.Mode == 0 {
|
|
cc.FileInfo.Mode = 0o755
|
|
}
|
|
if cc.FileInfo.MTime.IsZero() {
|
|
cc.FileInfo.MTime = mtime
|
|
}
|
|
|
|
// determine if we still need info
|
|
fileInfoAlreadyComplete := (!cc.FileInfo.MTime.IsZero() &&
|
|
cc.FileInfo.Mode != 0 &&
|
|
(cc.FileInfo.Size != 0 || (cc.Type == TypeDir || cc.Type == TypeImplicitDir)))
|
|
|
|
// only stat source when we actually need more information
|
|
if cc.Source != "" && !fileInfoAlreadyComplete {
|
|
info, err := os.Stat(cc.Source)
|
|
if err == nil {
|
|
if cc.FileInfo.MTime.IsZero() {
|
|
cc.FileInfo.MTime = info.ModTime()
|
|
}
|
|
if cc.FileInfo.Mode == 0 {
|
|
cc.FileInfo.Mode = info.Mode() &^ umask
|
|
}
|
|
cc.FileInfo.Size = info.Size()
|
|
}
|
|
}
|
|
|
|
if cc.FileInfo.MTime.IsZero() {
|
|
cc.FileInfo.MTime = mtime
|
|
}
|
|
return cc
|
|
}
|
|
|
|
// Name to part of the os.FileInfo interface
|
|
func (c *Content) Name() string {
|
|
return c.Source
|
|
}
|
|
|
|
// Size to part of the os.FileInfo interface
|
|
func (c *Content) Size() int64 {
|
|
return c.FileInfo.Size
|
|
}
|
|
|
|
// Mode to part of the os.FileInfo interface
|
|
func (c *Content) Mode() os.FileMode {
|
|
return c.FileInfo.Mode
|
|
}
|
|
|
|
// ModTime to part of the os.FileInfo interface
|
|
func (c *Content) ModTime() time.Time {
|
|
return c.FileInfo.MTime
|
|
}
|
|
|
|
// IsDir to part of the os.FileInfo interface
|
|
func (c *Content) IsDir() bool {
|
|
return c.Type == TypeDir || c.Type == TypeImplicitDir
|
|
}
|
|
|
|
// Sys to part of the os.FileInfo interface
|
|
func (c *Content) Sys() any {
|
|
return nil
|
|
}
|
|
|
|
func (c *Content) String() string {
|
|
var properties []string
|
|
if c.Source != "" {
|
|
properties = append(properties, "src="+c.Source)
|
|
}
|
|
if c.Destination != "" {
|
|
properties = append(properties, "dst="+c.Destination)
|
|
}
|
|
if c.Type != "" {
|
|
properties = append(properties, "type="+c.Type)
|
|
}
|
|
if c.Packager != "" {
|
|
properties = append(properties, "packager="+c.Packager)
|
|
}
|
|
if c.FileInfo != nil {
|
|
if c.FileInfo.Owner != "" {
|
|
properties = append(properties, "owner="+c.FileInfo.Owner)
|
|
}
|
|
if c.FileInfo.Group != "" {
|
|
properties = append(properties, "group="+c.FileInfo.Group)
|
|
}
|
|
if c.Mode() != 0 {
|
|
properties = append(properties, "mode="+c.Mode().String())
|
|
}
|
|
if !c.ModTime().IsZero() {
|
|
properties = append(properties, "modtime="+c.ModTime().String())
|
|
}
|
|
properties = append(properties, "size="+strconv.Itoa(int(c.FileInfo.Size)))
|
|
}
|
|
|
|
return fmt.Sprintf("Content(%s)", strings.Join(properties, ","))
|
|
}
|
|
|
|
// PrepareForPackager performs the following steps to prepare the contents for
|
|
// the provided packager:
|
|
//
|
|
// - It filters out content that is irrelevant for the specified packager
|
|
// - It expands globs (if enabled) and file trees
|
|
// - It adds implicit directories (parent directories of files)
|
|
// - It adds ownership and other file information if not specified directly
|
|
// - It applies the given umask if the file does not have a specific mode
|
|
// - It normalizes content source paths to be unix style paths
|
|
// - It normalizes content destination paths to be absolute paths with a trailing
|
|
// slash if the entry is a directory
|
|
//
|
|
// If no packager is specified, only the files that are relevant for any
|
|
// packager are considered.
|
|
func PrepareForPackager(
|
|
rawContents Contents,
|
|
umask fs.FileMode,
|
|
packager string,
|
|
disableGlobbing bool,
|
|
mtime time.Time,
|
|
) (Contents, error) {
|
|
contentMap := make(map[string]*Content)
|
|
|
|
for _, content := range rawContents {
|
|
if !isRelevantForPackager(packager, content) {
|
|
continue
|
|
}
|
|
|
|
switch content.Type {
|
|
case TypeDir:
|
|
// implicit directories at the same destination can just be overwritten
|
|
presentContent, destinationOccupied := contentMap[NormalizeAbsoluteDirPath(content.Destination)]
|
|
if destinationOccupied && presentContent.Type != TypeImplicitDir {
|
|
return nil, contentCollisionError(content, presentContent)
|
|
}
|
|
|
|
err := addParents(contentMap, content.Destination, mtime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cc := content.WithFileInfoDefaults(umask, mtime)
|
|
cc.Source = ToNixPath(cc.Source)
|
|
cc.Destination = NormalizeAbsoluteDirPath(cc.Destination)
|
|
contentMap[cc.Destination] = cc
|
|
case TypeImplicitDir:
|
|
// if there's an implicit directory, the contents probably already
|
|
// have been expanded so we can just ignore it, it will be created
|
|
// by another content element again anyway
|
|
case TypeRPMGhost, TypeSymlink, TypeRPMDoc, TypeRPMLicence, TypeRPMLicense, TypeRPMReadme, TypeDebChangelog:
|
|
presentContent, destinationOccupied := contentMap[NormalizeAbsoluteFilePath(content.Destination)]
|
|
if destinationOccupied {
|
|
return nil, contentCollisionError(content, presentContent)
|
|
}
|
|
|
|
err := addParents(contentMap, content.Destination, mtime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cc := content.WithFileInfoDefaults(umask, mtime)
|
|
cc.Source = ToNixPath(cc.Source)
|
|
cc.Destination = NormalizeAbsoluteFilePath(cc.Destination)
|
|
contentMap[cc.Destination] = cc
|
|
case TypeTree:
|
|
err := addTree(contentMap, content, umask, mtime)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("add tree: %w", err)
|
|
}
|
|
case TypeConfig, TypeConfigNoReplace, TypeConfigMissingOK, TypeFile, "":
|
|
globbed, err := glob.Glob(
|
|
filepath.ToSlash(content.Source),
|
|
filepath.ToSlash(content.Destination),
|
|
disableGlobbing,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := addGlobbedFiles(contentMap, globbed, content, umask, mtime); err != nil {
|
|
return nil, fmt.Errorf("add globbed files from %q: %w", content.Source, err)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("invalid content type: %s", content.Type)
|
|
}
|
|
}
|
|
|
|
res := make(Contents, 0, len(contentMap))
|
|
|
|
for _, content := range contentMap {
|
|
res = append(res, content)
|
|
}
|
|
|
|
sort.Sort(res)
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func isRelevantForPackager(packager string, content *Content) bool {
|
|
if packager == "" {
|
|
return true
|
|
}
|
|
|
|
if content.Packager != "" && content.Packager != packager {
|
|
return false
|
|
}
|
|
|
|
if packager != "rpm" &&
|
|
(content.Type == TypeRPMDoc || content.Type == TypeRPMLicence ||
|
|
content.Type == TypeRPMLicense || content.Type == TypeRPMReadme ||
|
|
content.Type == TypeRPMGhost) {
|
|
return false
|
|
}
|
|
|
|
if packager != "deb" && content.Type == TypeDebChangelog {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func addParents(contentMap map[string]*Content, path string, mtime time.Time) error {
|
|
for _, parent := range sortedParents(path) {
|
|
parent = NormalizeAbsoluteDirPath(parent)
|
|
// check for content collision and just overwrite previously created
|
|
// implicit directories
|
|
c, ok := contentMap[parent]
|
|
if ok {
|
|
// either we already created this directory as an explicit directory
|
|
// or as an implicit directory of another file
|
|
if c.Type == TypeDir || c.Type == TypeImplicitDir {
|
|
continue
|
|
}
|
|
|
|
return contentCollisionError(&Content{
|
|
Type: "parent directory for " + path,
|
|
Destination: parent,
|
|
}, c)
|
|
}
|
|
|
|
contentMap[parent] = &Content{
|
|
Destination: parent,
|
|
Type: TypeImplicitDir,
|
|
FileInfo: &ContentFileInfo{
|
|
Owner: "root",
|
|
Group: "root",
|
|
Mode: 0o755,
|
|
MTime: mtime,
|
|
},
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func sortedParents(dst string) []string {
|
|
paths := []string{}
|
|
base := strings.Trim(dst, "/")
|
|
for {
|
|
base = filepath.Dir(base)
|
|
if base == "." {
|
|
break
|
|
}
|
|
paths = append(paths, ToNixPath(base))
|
|
}
|
|
|
|
// reverse in place
|
|
for i := len(paths)/2 - 1; i >= 0; i-- {
|
|
oppositeIndex := len(paths) - 1 - i
|
|
paths[i], paths[oppositeIndex] = paths[oppositeIndex], paths[i]
|
|
}
|
|
|
|
return paths
|
|
}
|
|
|
|
func addGlobbedFiles(
|
|
all map[string]*Content,
|
|
globbed map[string]string,
|
|
origFile *Content,
|
|
umask fs.FileMode,
|
|
mtime time.Time,
|
|
) error {
|
|
for src, dst := range globbed {
|
|
dst = NormalizeAbsoluteFilePath(dst)
|
|
presentContent, destinationOccupied := all[dst]
|
|
if destinationOccupied {
|
|
c := *origFile
|
|
c.Destination = dst
|
|
return contentCollisionError(&c, presentContent)
|
|
}
|
|
|
|
if err := addParents(all, dst, mtime); err != nil {
|
|
return err
|
|
}
|
|
|
|
// if the file has a FileInfo, we need to copy it but recalculate its size
|
|
newFileInfo := origFile.FileInfo
|
|
if newFileInfo != nil {
|
|
newFileInfoVal := *newFileInfo
|
|
newFileInfoVal.Size = 0
|
|
newFileInfo = &newFileInfoVal
|
|
}
|
|
|
|
newFile := (&Content{
|
|
Destination: NormalizeAbsoluteFilePath(dst),
|
|
Source: ToNixPath(src),
|
|
Type: origFile.Type,
|
|
FileInfo: newFileInfo,
|
|
Packager: origFile.Packager,
|
|
}).WithFileInfoDefaults(umask, mtime)
|
|
if dst, err := os.Readlink(src); err == nil {
|
|
newFile.Source = dst
|
|
newFile.Type = TypeSymlink
|
|
}
|
|
|
|
all[dst] = newFile
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func addTree(
|
|
all map[string]*Content,
|
|
tree *Content,
|
|
umask os.FileMode,
|
|
mtime time.Time,
|
|
) error {
|
|
if tree.Destination != "/" && tree.Destination != "" {
|
|
presentContent, destinationOccupied := all[NormalizeAbsoluteDirPath(tree.Destination)]
|
|
if destinationOccupied && presentContent.Type != TypeImplicitDir {
|
|
return contentCollisionError(tree, presentContent)
|
|
}
|
|
}
|
|
|
|
err := addParents(all, tree.Destination, mtime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return filepath.WalkDir(tree.Source, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
relPath, err := filepath.Rel(tree.Source, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
destination := filepath.Join(tree.Destination, relPath)
|
|
|
|
c := &Content{
|
|
FileInfo: &ContentFileInfo{},
|
|
}
|
|
if tree.FileInfo != nil && !ownedByFilesystem(tree.Destination) {
|
|
c.FileInfo.Owner = tree.FileInfo.Owner
|
|
c.FileInfo.Group = tree.FileInfo.Group
|
|
}
|
|
|
|
switch {
|
|
case d.IsDir():
|
|
info, err := d.Info()
|
|
if err != nil {
|
|
return fmt.Errorf("get directory information: %w", err)
|
|
}
|
|
|
|
c.Type = TypeDir
|
|
c.Destination = NormalizeAbsoluteDirPath(destination)
|
|
c.FileInfo.Mode = info.Mode() &^ umask
|
|
c.FileInfo.MTime = info.ModTime()
|
|
if ownedByFilesystem(c.Destination) {
|
|
c.Type = TypeImplicitDir
|
|
}
|
|
case d.Type()&os.ModeSymlink != 0:
|
|
linkDestination, err := os.Readlink(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.Type = TypeSymlink
|
|
c.Source = filepath.ToSlash(strings.TrimPrefix(linkDestination, filepath.VolumeName(linkDestination)))
|
|
c.Destination = NormalizeAbsoluteFilePath(destination)
|
|
default:
|
|
c.Type = TypeFile
|
|
c.Source = path
|
|
c.Destination = NormalizeAbsoluteFilePath(destination)
|
|
c.FileInfo.Mode = d.Type() &^ umask
|
|
}
|
|
|
|
if tree.FileInfo != nil && tree.FileInfo.Mode != 0 && c.Type != TypeSymlink {
|
|
c.FileInfo.Mode = tree.FileInfo.Mode
|
|
}
|
|
|
|
all[c.Destination] = c.WithFileInfoDefaults(umask, mtime)
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
var ErrContentCollision = fmt.Errorf("content collision")
|
|
|
|
func contentCollisionError(newc *Content, present *Content) error {
|
|
var presentSource string
|
|
if present.Source != "" {
|
|
presentSource = " with source " + present.Source
|
|
}
|
|
|
|
return fmt.Errorf("adding %s at destination %s: "+
|
|
"%s%s is already present at this destination: %w",
|
|
newc.Type, newc.Destination, present.Type, presentSource, ErrContentCollision,
|
|
)
|
|
}
|
|
|
|
// ToNixPath converts the given path to a nix-style path.
|
|
//
|
|
// Windows-style path separators are considered escape
|
|
// characters by some libraries, which can cause issues.
|
|
func ToNixPath(path string) string {
|
|
return filepath.ToSlash(filepath.Clean(path))
|
|
}
|
|
|
|
// As relative path converts a path to an explicitly relative path starting with
|
|
// a dot (e.g. it converts /foo -> ./foo and foo -> ./foo).
|
|
func AsExplicitRelativePath(path string) string {
|
|
return "./" + AsRelativePath(path)
|
|
}
|
|
|
|
// AsRelativePath converts a path to a relative path without a "./" prefix. This
|
|
// function leaves trailing slashes to indicate that the path refers to a
|
|
// directory, and converts the path to Unix path.
|
|
func AsRelativePath(path string) string {
|
|
cleanedPath := strings.TrimLeft(ToNixPath(path), "/")
|
|
if len(cleanedPath) > 1 && strings.HasSuffix(path, "/") {
|
|
return cleanedPath + "/"
|
|
}
|
|
return cleanedPath
|
|
}
|
|
|
|
// NormalizeAbsoluteFilePath returns an absolute cleaned path separated by
|
|
// slashes.
|
|
func NormalizeAbsoluteFilePath(src string) string {
|
|
return ToNixPath(filepath.Join("/", src))
|
|
}
|
|
|
|
// normalizeFirPath is linke NormalizeAbsoluteFilePath with a trailing slash.
|
|
func NormalizeAbsoluteDirPath(path string) string {
|
|
return NormalizeAbsoluteFilePath(strings.TrimRight(path, "/")) + "/"
|
|
}
|