github.com/goreleaser/nfpm/v2@v2.44.0/files/files.go (about) 1 package files 2 3 import ( 4 "fmt" 5 "io/fs" 6 "os" 7 "path/filepath" 8 "sort" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/goreleaser/nfpm/v2/internal/glob" 14 ) 15 16 const ( 17 // TypeFile is the type of a regular file. This is also the type that is 18 // implied when no type is specified. 19 TypeFile = "file" 20 // TypeDir is the type of a directory that is explicitly added in order to 21 // declare ownership or non-standard permission. 22 TypeDir = "dir" 23 /// TypeImplicitDir is the type of a directory that is implicitly added as a 24 //parent of a file. 25 TypeImplicitDir = "implicit dir" 26 // TypeTree is the type of a whole directory tree structure. 27 TypeTree = "tree" 28 // TypeSymlink is the type of a symlink that is created at the destination 29 // path and points to the source path. 30 TypeSymlink = "symlink" 31 // TypeConfig is the type of a configuration file that may be changed by the 32 // user of the package. 33 TypeConfig = "config" 34 // TypeConfigNoReplace is like TypeConfig with an added noreplace directive 35 // that is respected by RPM-based distributions. 36 // For all other package formats it is handled exactly like TypeConfig. 37 TypeConfigNoReplace = "config|noreplace" 38 // TypeConfigMissingOK is like TypeConfig with an added missingok directive 39 // that is respected by RPM-based distributions. 40 // For all other package formats it is handled exactly like TypeConfig. 41 TypeConfigMissingOK = "config|missingok" 42 // TypeGhost is the type of an RPM ghost file which is ignored by other packagers. 43 TypeRPMGhost = "ghost" 44 // TypeRPMDoc is the type of an RPM doc file which is ignored by other packagers. 45 TypeRPMDoc = "doc" 46 // TypeRPMLicence is the type of an RPM licence file which is ignored by other packagers. 47 TypeRPMLicence = "licence" 48 // TypeRPMLicense a different spelling of TypeRPMLicence. 49 TypeRPMLicense = "license" 50 // TypeRPMReadme is the type of an RPM readme file which is ignored by other packagers. 51 TypeRPMReadme = "readme" 52 // TypeDebChangelog is the type of a Debian changelog archive file which is 53 // ignored by other packagers. This type should never be set for a content 54 // entry as it is automatically added when a changelog is configred. 55 TypeDebChangelog = "debian changelog" 56 ) 57 58 // Content describes the source and destination 59 // of one file to copy into a package. 60 type Content struct { 61 Source string `yaml:"src,omitempty" json:"src,omitempty"` 62 Destination string `yaml:"dst" json:"dst"` 63 Type string `yaml:"type,omitempty" json:"type,omitempty" jsonschema:"enum=symlink,enum=ghost,enum=config,enum=config|noreplace,enum=dir,enum=tree,enum=,default="` 64 Packager string `yaml:"packager,omitempty" json:"packager,omitempty"` 65 FileInfo *ContentFileInfo `yaml:"file_info,omitempty" json:"file_info,omitempty"` 66 Expand bool `yaml:"expand,omitempty" json:"expand,omitempty"` 67 } 68 69 type ContentFileInfo struct { 70 Owner string `yaml:"owner,omitempty" json:"owner,omitempty"` 71 Group string `yaml:"group,omitempty" json:"group,omitempty"` 72 Mode os.FileMode `yaml:"mode,omitempty" json:"mode,omitempty"` 73 MTime time.Time `yaml:"mtime,omitempty" json:"mtime,omitempty"` 74 Size int64 `yaml:"-" json:"-"` 75 } 76 77 // Contents list of Content to process. 78 type Contents []*Content 79 80 func (c Contents) Len() int { 81 return len(c) 82 } 83 84 func (c Contents) Swap(i, j int) { 85 c[i], c[j] = c[j], c[i] 86 } 87 88 func (c Contents) Less(i, j int) bool { 89 a, b := c[i], c[j] 90 91 if a.Destination != b.Destination { 92 return a.Destination < b.Destination 93 } 94 95 if a.Type != b.Type { 96 return a.Type < b.Type 97 } 98 99 return a.Packager < b.Packager 100 } 101 102 func (c Contents) ContainsDestination(dst string) bool { 103 for _, content := range c { 104 if strings.TrimRight(content.Destination, "/") == strings.TrimRight(dst, "/") { 105 return true 106 } 107 } 108 109 return false 110 } 111 112 func (c *Content) WithFileInfoDefaults(umask fs.FileMode, mtime time.Time) *Content { 113 cc := &Content{ 114 Source: c.Source, 115 Destination: c.Destination, 116 Type: c.Type, 117 Packager: c.Packager, 118 FileInfo: c.FileInfo, 119 } 120 if cc.Type == "" { 121 cc.Type = TypeFile 122 } 123 if cc.FileInfo == nil { 124 cc.FileInfo = &ContentFileInfo{} 125 } 126 if cc.FileInfo.Owner == "" { 127 cc.FileInfo.Owner = "root" 128 } 129 if cc.FileInfo.Group == "" { 130 cc.FileInfo.Group = "root" 131 } 132 if (cc.Type == TypeDir || cc.Type == TypeImplicitDir) && cc.FileInfo.Mode == 0 { 133 cc.FileInfo.Mode = 0o755 134 } 135 if cc.FileInfo.MTime.IsZero() { 136 cc.FileInfo.MTime = mtime 137 } 138 139 // determine if we still need info 140 fileInfoAlreadyComplete := (!cc.FileInfo.MTime.IsZero() && 141 cc.FileInfo.Mode != 0 && 142 (cc.FileInfo.Size != 0 || (cc.Type == TypeDir || cc.Type == TypeImplicitDir))) 143 144 // only stat source when we actually need more information 145 if cc.Source != "" && !fileInfoAlreadyComplete { 146 info, err := os.Stat(cc.Source) 147 if err == nil { 148 if cc.FileInfo.MTime.IsZero() { 149 // if we can stat the file and mtime not set, use original 150 // file's mtime 151 cc.FileInfo.MTime = info.ModTime() 152 } 153 if cc.FileInfo.Mode == 0 { 154 cc.FileInfo.Mode = info.Mode() &^ umask 155 } 156 cc.FileInfo.Size = info.Size() 157 } 158 } 159 160 // finally, if mtime is still 0, set time.Now() 161 if cc.FileInfo.MTime.IsZero() { 162 cc.FileInfo.MTime = time.Now() 163 } 164 return cc 165 } 166 167 // Name to part of the os.FileInfo interface 168 func (c *Content) Name() string { 169 return c.Source 170 } 171 172 // Size to part of the os.FileInfo interface 173 func (c *Content) Size() int64 { 174 return c.FileInfo.Size 175 } 176 177 // Mode to part of the os.FileInfo interface 178 func (c *Content) Mode() os.FileMode { 179 return c.FileInfo.Mode 180 } 181 182 // ModTime to part of the os.FileInfo interface 183 func (c *Content) ModTime() time.Time { 184 return c.FileInfo.MTime 185 } 186 187 // IsDir to part of the os.FileInfo interface 188 func (c *Content) IsDir() bool { 189 return c.Type == TypeDir || c.Type == TypeImplicitDir 190 } 191 192 // Sys to part of the os.FileInfo interface 193 func (c *Content) Sys() any { 194 return nil 195 } 196 197 func (c *Content) String() string { 198 var properties []string 199 if c.Source != "" { 200 properties = append(properties, "src="+c.Source) 201 } 202 if c.Destination != "" { 203 properties = append(properties, "dst="+c.Destination) 204 } 205 if c.Type != "" { 206 properties = append(properties, "type="+c.Type) 207 } 208 if c.Packager != "" { 209 properties = append(properties, "packager="+c.Packager) 210 } 211 if c.FileInfo != nil { 212 if c.FileInfo.Owner != "" { 213 properties = append(properties, "owner="+c.FileInfo.Owner) 214 } 215 if c.FileInfo.Group != "" { 216 properties = append(properties, "group="+c.FileInfo.Group) 217 } 218 if c.Mode() != 0 { 219 properties = append(properties, "mode="+c.Mode().String()) 220 } 221 if !c.ModTime().IsZero() { 222 properties = append(properties, "modtime="+c.ModTime().String()) 223 } 224 properties = append(properties, "size="+strconv.Itoa(int(c.FileInfo.Size))) 225 } 226 227 return fmt.Sprintf("Content(%s)", strings.Join(properties, ",")) 228 } 229 230 // PrepareForPackager performs the following steps to prepare the contents for 231 // the provided packager: 232 // 233 // - It filters out content that is irrelevant for the specified packager 234 // - It expands globs (if enabled) and file trees 235 // - It adds implicit directories (parent directories of files) 236 // - It adds ownership and other file information if not specified directly 237 // - It applies the given umask if the file does not have a specific mode 238 // - It normalizes content source paths to be unix style paths 239 // - It normalizes content destination paths to be absolute paths with a trailing 240 // slash if the entry is a directory 241 // 242 // If no packager is specified, only the files that are relevant for any 243 // packager are considered. 244 func PrepareForPackager( 245 rawContents Contents, 246 umask fs.FileMode, 247 packager string, 248 disableGlobbing bool, 249 mtime time.Time, 250 ) (Contents, error) { 251 contentMap := make(map[string]*Content) 252 253 for _, content := range rawContents { 254 if !isRelevantForPackager(packager, content) { 255 continue 256 } 257 258 switch content.Type { 259 case TypeDir: 260 // implicit directories at the same destination can just be overwritten 261 presentContent, destinationOccupied := contentMap[NormalizeAbsoluteDirPath(content.Destination)] 262 if destinationOccupied && presentContent.Type != TypeImplicitDir { 263 return nil, contentCollisionError(content, presentContent) 264 } 265 266 err := addParents(contentMap, content.Destination, mtime, nil) 267 if err != nil { 268 return nil, err 269 } 270 271 cc := content.WithFileInfoDefaults(umask, mtime) 272 cc.Source = ToNixPath(cc.Source) 273 cc.Destination = NormalizeAbsoluteDirPath(cc.Destination) 274 contentMap[cc.Destination] = cc 275 case TypeImplicitDir: 276 // if there's an implicit directory, the contents probably already 277 // have been expanded so we can just ignore it, it will be created 278 // by another content element again anyway 279 case TypeRPMGhost, TypeSymlink, TypeRPMDoc, TypeRPMLicence, TypeRPMLicense, TypeRPMReadme, TypeDebChangelog: 280 presentContent, destinationOccupied := contentMap[NormalizeAbsoluteFilePath(content.Destination)] 281 if destinationOccupied { 282 return nil, contentCollisionError(content, presentContent) 283 } 284 285 err := addParents(contentMap, content.Destination, mtime, nil) 286 if err != nil { 287 return nil, err 288 } 289 290 cc := content.WithFileInfoDefaults(umask, mtime) 291 cc.Source = ToNixPath(cc.Source) 292 cc.Destination = NormalizeAbsoluteFilePath(cc.Destination) 293 contentMap[cc.Destination] = cc 294 case TypeTree: 295 err := addTree(contentMap, content, umask, mtime) 296 if err != nil { 297 return nil, fmt.Errorf("add tree: %w", err) 298 } 299 case TypeConfig, TypeConfigNoReplace, TypeConfigMissingOK, TypeFile, "": 300 globbed, err := glob.Glob( 301 filepath.ToSlash(content.Source), 302 filepath.ToSlash(content.Destination), 303 disableGlobbing, 304 ) 305 if err != nil { 306 return nil, err 307 } 308 309 if err := addGlobbedFiles(contentMap, globbed, content, umask, mtime); err != nil { 310 return nil, fmt.Errorf("add globbed files from %q: %w", content.Source, err) 311 } 312 default: 313 return nil, fmt.Errorf("invalid content type: %s", content.Type) 314 } 315 } 316 317 res := make(Contents, 0, len(contentMap)) 318 319 for _, content := range contentMap { 320 res = append(res, content) 321 } 322 323 sort.Sort(res) 324 325 return res, nil 326 } 327 328 func isRelevantForPackager(packager string, content *Content) bool { 329 if packager == "" { 330 return true 331 } 332 333 if content.Packager != "" && content.Packager != packager { 334 return false 335 } 336 337 if packager != "rpm" && 338 (content.Type == TypeRPMDoc || content.Type == TypeRPMLicence || 339 content.Type == TypeRPMLicense || content.Type == TypeRPMReadme || 340 content.Type == TypeRPMGhost) { 341 return false 342 } 343 344 if packager != "deb" && content.Type == TypeDebChangelog { 345 return false 346 } 347 348 return true 349 } 350 351 func addParents(contentMap map[string]*Content, path string, mtime time.Time, fileInfo *ContentFileInfo) error { 352 for _, parent := range sortedParents(path) { 353 parent = NormalizeAbsoluteDirPath(parent) 354 // check for content collision and just overwrite previously created 355 // implicit directories 356 c, ok := contentMap[parent] 357 if ok { 358 // either we already created this directory as an explicit directory 359 // or as an implicit directory of another file 360 if c.Type == TypeDir || c.Type == TypeImplicitDir { 361 continue 362 } 363 364 return contentCollisionError(&Content{ 365 Type: "parent directory for " + path, 366 Destination: parent, 367 }, c) 368 } 369 370 owner := "root" 371 group := "root" 372 373 // Use provided ownership for directories that are not owned by the filesystem 374 if fileInfo != nil && !ownedByFilesystem(parent) { 375 if fileInfo.Owner != "" { 376 owner = fileInfo.Owner 377 } 378 if fileInfo.Group != "" { 379 group = fileInfo.Group 380 } 381 } 382 383 contentMap[parent] = &Content{ 384 Destination: parent, 385 Type: TypeImplicitDir, 386 FileInfo: &ContentFileInfo{ 387 Owner: owner, 388 Group: group, 389 Mode: 0o755, 390 MTime: mtime, 391 }, 392 } 393 } 394 395 return nil 396 } 397 398 func sortedParents(dst string) []string { 399 paths := []string{} 400 base := strings.Trim(dst, "/") 401 for { 402 base = filepath.Dir(base) 403 if base == "." { 404 break 405 } 406 paths = append(paths, ToNixPath(base)) 407 } 408 409 // reverse in place 410 for i := len(paths)/2 - 1; i >= 0; i-- { 411 oppositeIndex := len(paths) - 1 - i 412 paths[i], paths[oppositeIndex] = paths[oppositeIndex], paths[i] 413 } 414 415 return paths 416 } 417 418 func addGlobbedFiles( 419 all map[string]*Content, 420 globbed map[string]string, 421 origFile *Content, 422 umask fs.FileMode, 423 mtime time.Time, 424 ) error { 425 for src, dst := range globbed { 426 dst = NormalizeAbsoluteFilePath(dst) 427 presentContent, destinationOccupied := all[dst] 428 if destinationOccupied { 429 c := *origFile 430 c.Destination = dst 431 return contentCollisionError(&c, presentContent) 432 } 433 434 if err := addParents(all, dst, mtime, origFile.FileInfo); err != nil { 435 return err 436 } 437 438 // if the file has a FileInfo, we need to copy it but recalculate its size 439 newFileInfo := origFile.FileInfo 440 if newFileInfo != nil { 441 newFileInfoVal := *newFileInfo 442 newFileInfoVal.Size = 0 443 newFileInfo = &newFileInfoVal 444 } 445 446 newFile := (&Content{ 447 Destination: NormalizeAbsoluteFilePath(dst), 448 Source: ToNixPath(src), 449 Type: origFile.Type, 450 FileInfo: newFileInfo, 451 Packager: origFile.Packager, 452 }).WithFileInfoDefaults(umask, mtime) 453 if dst, err := os.Readlink(src); err == nil { 454 newFile.Source = dst 455 newFile.Type = TypeSymlink 456 } 457 458 all[dst] = newFile 459 } 460 461 return nil 462 } 463 464 func addTree( 465 all map[string]*Content, 466 tree *Content, 467 umask os.FileMode, 468 mtime time.Time, 469 ) error { 470 if tree.Destination != "/" && tree.Destination != "" { 471 presentContent, destinationOccupied := all[NormalizeAbsoluteDirPath(tree.Destination)] 472 if destinationOccupied && presentContent.Type != TypeImplicitDir { 473 return contentCollisionError(tree, presentContent) 474 } 475 } 476 477 err := addParents(all, tree.Destination, mtime, tree.FileInfo) 478 if err != nil { 479 return err 480 } 481 482 return filepath.WalkDir(tree.Source, func(path string, d fs.DirEntry, err error) error { 483 if err != nil { 484 return err 485 } 486 487 relPath, err := filepath.Rel(tree.Source, path) 488 if err != nil { 489 return err 490 } 491 492 destination := filepath.Join(tree.Destination, relPath) 493 494 c := &Content{ 495 FileInfo: &ContentFileInfo{}, 496 } 497 if tree.FileInfo != nil && !ownedByFilesystem(c.Destination) { 498 c.FileInfo.Owner = tree.FileInfo.Owner 499 c.FileInfo.Group = tree.FileInfo.Group 500 } 501 502 switch { 503 case d.IsDir(): 504 info, err := d.Info() 505 if err != nil { 506 return fmt.Errorf("get directory information: %w", err) 507 } 508 509 c.Type = TypeDir 510 c.Destination = NormalizeAbsoluteDirPath(destination) 511 c.FileInfo.Mode = info.Mode() &^ umask 512 c.FileInfo.MTime = info.ModTime() 513 if ownedByFilesystem(c.Destination) { 514 c.Type = TypeImplicitDir 515 } 516 case d.Type()&os.ModeSymlink != 0: 517 linkDestination, err := os.Readlink(path) 518 if err != nil { 519 return err 520 } 521 522 c.Type = TypeSymlink 523 c.Source = filepath.ToSlash(strings.TrimPrefix(linkDestination, filepath.VolumeName(linkDestination))) 524 c.Destination = NormalizeAbsoluteFilePath(destination) 525 default: 526 c.Type = TypeFile 527 c.Source = path 528 c.Destination = NormalizeAbsoluteFilePath(destination) 529 c.FileInfo.Mode = d.Type() &^ umask 530 } 531 532 if tree.FileInfo != nil && tree.FileInfo.Mode != 0 && c.Type != TypeSymlink { 533 c.FileInfo.Mode = tree.FileInfo.Mode 534 } 535 536 all[c.Destination] = c.WithFileInfoDefaults(umask, mtime) 537 538 return nil 539 }) 540 } 541 542 var ErrContentCollision = fmt.Errorf("content collision") 543 544 func contentCollisionError(newc *Content, present *Content) error { 545 var presentSource string 546 if present.Source != "" { 547 presentSource = " with source " + present.Source 548 } 549 550 return fmt.Errorf("adding %s at destination %s: "+ 551 "%s%s is already present at this destination: %w", 552 newc.Type, newc.Destination, present.Type, presentSource, ErrContentCollision, 553 ) 554 } 555 556 // ToNixPath converts the given path to a nix-style path. 557 // 558 // Windows-style path separators are considered escape 559 // characters by some libraries, which can cause issues. 560 func ToNixPath(path string) string { 561 return filepath.ToSlash(filepath.Clean(path)) 562 } 563 564 // As relative path converts a path to an explicitly relative path starting with 565 // a dot (e.g. it converts /foo -> ./foo and foo -> ./foo). 566 func AsExplicitRelativePath(path string) string { 567 return "./" + AsRelativePath(path) 568 } 569 570 // AsRelativePath converts a path to a relative path without a "./" prefix. This 571 // function leaves trailing slashes to indicate that the path refers to a 572 // directory, and converts the path to Unix path. 573 func AsRelativePath(path string) string { 574 cleanedPath := strings.TrimLeft(ToNixPath(path), "/") 575 if len(cleanedPath) > 1 && strings.HasSuffix(path, "/") { 576 return cleanedPath + "/" 577 } 578 return cleanedPath 579 } 580 581 // NormalizeAbsoluteFilePath returns an absolute cleaned path separated by 582 // slashes. 583 func NormalizeAbsoluteFilePath(src string) string { 584 return ToNixPath(filepath.Join("/", src)) 585 } 586 587 // normalizeFirPath is linke NormalizeAbsoluteFilePath with a trailing slash. 588 func NormalizeAbsoluteDirPath(path string) string { 589 return NormalizeAbsoluteFilePath(strings.TrimRight(path, "/")) + "/" 590 }