github.com/goreleaser/nfpm/v2@v2.44.0/ipk/ipk.go (about) 1 // Package ipk implements nfpm.Packager providing .ipk bindings. 2 // 3 // IPK is a package format used by the opkg package manager, which is very 4 // similar to the Debian package format. Generally, the package format is 5 // stripped down and simplified compared to the Debian package format. 6 // Yocto/OpenEmbedded/OpenWRT uses the IPK format for its package management. 7 package ipk 8 9 import ( 10 "archive/tar" 11 "bufio" 12 "bytes" 13 "fmt" 14 "io" 15 "strings" 16 "text/template" 17 "time" 18 19 "github.com/goreleaser/nfpm/v2" 20 "github.com/goreleaser/nfpm/v2/deprecation" 21 "github.com/goreleaser/nfpm/v2/files" 22 "github.com/goreleaser/nfpm/v2/internal/modtime" 23 ) 24 25 const packagerName = "ipk" 26 27 // nolint: gochecknoinits 28 func init() { 29 nfpm.RegisterPackager(packagerName, Default) 30 } 31 32 // nolint: gochecknoglobals 33 var archToIPK = map[string]string{ 34 // all --> all 35 "386": "i386", 36 "amd64": "x86_64", 37 "arm64": "arm64", 38 "arm5": "armel", 39 "arm6": "armhf", 40 "arm7": "armhf", 41 "mips64le": "mips64el", 42 "mipsle": "mipsel", 43 "ppc64le": "ppc64el", 44 "s390": "s390x", 45 } 46 47 func ensureValidArch(info *nfpm.Info) *nfpm.Info { 48 if info.IPK.Arch != "" { 49 info.Arch = info.IPK.Arch 50 } else if arch, ok := archToIPK[info.Arch]; ok { 51 info.Arch = arch 52 } 53 54 return info 55 } 56 57 // Default ipk packager. 58 // nolint: gochecknoglobals 59 var Default = &IPK{} 60 61 // IPK is a ipk packager implementation. 62 type IPK struct{} 63 64 // ConventionalFileName returns a file name according 65 // to the conventions for ipk packages. Ipk packages generally follow 66 // the conventions set by debian. See: 67 // https://manpages.debian.org/buster/dpkg-dev/dpkg-name.1.en.html 68 func (*IPK) ConventionalFileName(info *nfpm.Info) string { 69 info = ensureValidArch(info) 70 71 version := info.Version 72 if info.Prerelease != "" { 73 version += "~" + info.Prerelease 74 } 75 76 if info.VersionMetadata != "" { 77 version += "+" + info.VersionMetadata 78 } 79 80 if info.Release != "" { 81 version += "-" + info.Release 82 } 83 84 // package_version_architecture.package-type 85 return fmt.Sprintf("%s_%s_%s.ipk", info.Name, version, info.Arch) 86 } 87 88 // ConventionalExtension returns the file name conventionally used for IPK packages 89 func (*IPK) ConventionalExtension() string { 90 return ".ipk" 91 } 92 93 // SetPackagerDefaults sets the default values for the IPK packager. 94 func (*IPK) SetPackagerDefaults(info *nfpm.Info) { 95 // Priority should be set on all packages per: 96 // https://www.debian.org/doc/debian-policy/ch-archive.html#priorities 97 // "optional" seems to be the safe/sane default here 98 if info.Priority == "" { 99 info.Priority = "optional" 100 } 101 102 // The safe thing here feels like defaulting to something like below. 103 // That will prevent existing configs from breaking anyway... Wondering 104 // if in the long run we should be more strict about this and error when 105 // not set? 106 if strings.TrimSpace(info.Maintainer) == "" { 107 deprecation.Println("Leaving the 'maintainer' field unset will not be allowed in a future version") 108 info.Maintainer = "Unset Maintainer <unset@localhost>" 109 } 110 } 111 112 // Package writes a new ipk package to the given writer using the given info. 113 func (d *IPK) Package(info *nfpm.Info, ipk io.Writer) error { 114 info = ensureValidArch(info) 115 116 if err := nfpm.PrepareForPackager(info, packagerName); err != nil { 117 return err 118 } 119 120 // Set up some ipk specific defaults 121 d.SetPackagerDefaults(info) 122 123 // Strip out any custom fields that are disallowed. 124 stripDisallowedFields(info) 125 126 contents, err := newTGZ("ipk", 127 func(tw *tar.Writer) error { 128 return createIPK(info, tw) 129 }, 130 ) 131 if err != nil { 132 return err 133 } 134 135 _, err = ipk.Write(contents) 136 137 return err 138 } 139 140 // createIPK creates a new ipk package using the given tar writer and info. 141 func createIPK(info *nfpm.Info, ipk *tar.Writer) error { 142 var installSize int64 143 144 data, err := newTGZ("data.tar.gz", 145 func(tw *tar.Writer) error { 146 var err error 147 installSize, err = populateDataTar(info, tw) 148 return err 149 }, 150 ) 151 if err != nil { 152 return err 153 } 154 155 control, err := newTGZ("control.tar.gz", 156 func(tw *tar.Writer) error { 157 return populateControlTar(info, tw, installSize) 158 }, 159 ) 160 if err != nil { 161 return err 162 } 163 164 mtime := modtime.Get(info.MTime) 165 166 if err := writeToFile(ipk, "debian-binary", []byte("2.0\n"), mtime); err != nil { 167 return err 168 } 169 170 if err := writeToFile(ipk, "control.tar.gz", control, mtime); err != nil { 171 return err 172 } 173 174 if err := writeToFile(ipk, "data.tar.gz", data, mtime); err != nil { 175 return err 176 } 177 178 return nil 179 } 180 181 // populateDataTar populates the data tarball with the files specified in the info. 182 func populateDataTar(info *nfpm.Info, tw *tar.Writer) (instSize int64, err error) { 183 // create files and implicit directories 184 for _, file := range info.Contents { 185 var size int64 186 187 switch file.Type { 188 case files.TypeDir, files.TypeImplicitDir: 189 err = tw.WriteHeader( 190 &tar.Header{ 191 Name: files.AsExplicitRelativePath(file.Destination), 192 Typeflag: tar.TypeDir, 193 Format: tar.FormatGNU, 194 ModTime: modtime.Get(info.MTime), 195 Mode: int64(file.FileInfo.Mode), 196 Uname: file.FileInfo.Owner, 197 Gname: file.FileInfo.Group, 198 }) 199 case files.TypeSymlink: 200 err = tw.WriteHeader( 201 &tar.Header{ 202 Name: files.AsExplicitRelativePath(file.Destination), 203 Typeflag: tar.TypeSymlink, 204 Format: tar.FormatGNU, 205 ModTime: modtime.Get(info.MTime), 206 Linkname: file.Source, 207 }) 208 case files.TypeFile, files.TypeTree, files.TypeConfig, files.TypeConfigNoReplace, files.TypeConfigMissingOK: 209 size, err = writeFile(tw, file) 210 default: 211 // ignore everything else 212 } 213 if err != nil { 214 return 0, err 215 } 216 instSize += size 217 } 218 219 return instSize, nil 220 } 221 222 // getScripts returns the scripts for the given info. 223 func getScripts(info *nfpm.Info, mtime time.Time) []files.Content { 224 return []files.Content{ 225 { 226 Destination: "preinst", 227 Source: info.Scripts.PreInstall, 228 FileInfo: &files.ContentFileInfo{ 229 Mode: 0o755, 230 MTime: mtime, 231 }, 232 }, { 233 Destination: "postinst", 234 Source: info.Scripts.PostInstall, 235 FileInfo: &files.ContentFileInfo{ 236 Mode: 0o755, 237 MTime: mtime, 238 }, 239 }, { 240 Destination: "prerm", 241 Source: info.Scripts.PreRemove, 242 FileInfo: &files.ContentFileInfo{ 243 Mode: 0o755, 244 MTime: mtime, 245 }, 246 }, { 247 Destination: "postrm", 248 Source: info.Scripts.PostRemove, 249 FileInfo: &files.ContentFileInfo{ 250 Mode: 0o755, 251 MTime: mtime, 252 }, 253 }, 254 } 255 } 256 257 // populateControlTar populates the control tarball with the control files defined 258 // in the info. 259 func populateControlTar(info *nfpm.Info, out *tar.Writer, instSize int64) error { 260 var body bytes.Buffer 261 262 cd := controlData{ 263 Info: info, 264 InstalledSize: instSize / 1024, 265 } 266 267 if err := renderControl(&body, cd); err != nil { 268 return err 269 } 270 271 mtime := modtime.Get(info.MTime) 272 if err := writeToFile(out, "./control", body.Bytes(), mtime); err != nil { 273 return err 274 } 275 if err := writeToFile(out, "./conffiles", conffiles(info), mtime); err != nil { 276 return err 277 } 278 279 scripts := getScripts(info, mtime) 280 for _, file := range scripts { 281 if file.Source != "" { 282 if _, err := writeFile(out, &file); err != nil { 283 return err 284 } 285 } 286 } 287 return nil 288 } 289 290 // conffiles returns the conffiles file bytes for the given info. 291 func conffiles(info *nfpm.Info) []byte { 292 // nolint: prealloc 293 var confs []string 294 for _, file := range info.Contents { 295 switch file.Type { 296 case files.TypeConfig, files.TypeConfigNoReplace, files.TypeConfigMissingOK: 297 confs = append(confs, files.NormalizeAbsoluteFilePath(file.Destination)) 298 } 299 } 300 return []byte(strings.Join(confs, "\n") + "\n") 301 } 302 303 // The ipk format is not formally defined, but it is similar to the deb format. 304 // The two sources that were used to create this template are: 305 // - https://git.yoctoproject.org/opkg/ 306 // - https://github.com/openwrt/opkg-lede 307 // 308 // Supported Fields 309 // 310 // R = Required 311 // O = Optional 312 // e = Extra 313 // - = Not Supported/Ignored/Extra 314 // 315 // 316 // OpenWRT Yocto 317 // | | 318 // | Field | W | Y | Status | 319 // |----------------|---|---|--------| 320 // | ABIVersion | O | - | ✓ 321 // | Alternatives | O | - | ✓ 322 // | Architecture | R | R | ✓ 323 // | Auto-Installed | O | O | ✓ 324 // | Conffiles | O | O | not needed since config files are listed in .conffiles 325 // | Conflicts | O | O | ✓ 326 // | Depends | R | R | ✓ 327 // | Description | R | R | ✓ 328 // | Essential | O | O | ✓ 329 // | Filename | - | - | an opkg field, not a package field 330 // | Homepage | e | e | ✓ 331 // | Installed-Size | O | O | ✓ 332 // | Installed-Time | - | - | an opkg field, not a package field 333 // | License | e | e | ✓ 334 // | Maintainer | R | R | ✓ 335 // | MD5sum | - | - | insecure, not supported 336 // | Package | R | R | ✓ 337 // | Pre-Depends | e | O | ✓ 338 // | Priority | R | R | ✓ 339 // | Provides | O | O | ✓ 340 // | Recommends | O | O | ✓ 341 // | Replaces | O | O | ✓ 342 // | Section | O | O | ✓ 343 // | SHA256sum | - | - | an opkg field, not a package field 344 // | Size | - | - | an opkg field, not a package field 345 // | Source | - | - | use the Fields field 346 // | Status | - | - | an opkg state, not a package field 347 // | Suggests | O | O | ✓ 348 // | Tags | O | O | ✓ 349 // | Vendor | e | e | ✓ 350 // | Version | R | R | ✓ 351 // 352 // If any values in user supplied Fields are found to be duplicates of the above 353 // fields, they will be stripped out. 354 355 // nolint: gochecknoglobals 356 var controlFields = []string{ 357 "ABIVersion", 358 "Alternatives", 359 "Architecture", 360 "Auto-Installed", 361 "Conffiles", 362 "Conflicts", 363 "Depends", 364 "Description", 365 "Essential", 366 "Filename", 367 "Homepage", 368 "Installed-Size", 369 "Installed-Time", 370 "License", 371 "Maintainer", 372 "MD5sum", 373 "Package", 374 "Pre-Depends", 375 "Priority", 376 "Provides", 377 "Recommends", 378 "Replaces", 379 "Section", 380 "SHA256sum", 381 "Size", 382 // "Source", Allowed 383 "Status", 384 "Suggests", 385 "Tags", 386 "Vendor", 387 "Version", 388 } 389 390 // stripDisallowedFields strips out any fields that are disallowed in the ipk 391 // format, ignoring case. 392 func stripDisallowedFields(info *nfpm.Info) { 393 for key := range info.IPK.Fields { 394 for _, disallowed := range controlFields { 395 if strings.EqualFold(key, disallowed) { 396 delete(info.IPK.Fields, key) 397 } 398 } 399 } 400 } 401 402 const controlTemplate = ` 403 {{- /* Mandatory fields */ -}} 404 Architecture: {{.Info.Arch}} 405 Description: {{multiline .Info.Description}} 406 Maintainer: {{.Info.Maintainer}} 407 Package: {{.Info.Name}} 408 Priority: {{.Info.Priority}} 409 Version: {{ if .Info.Epoch}}{{ .Info.Epoch }}:{{ end }}{{.Info.Version}} 410 {{- if .Info.Prerelease}}~{{ .Info.Prerelease }}{{- end }} 411 {{- if .Info.VersionMetadata}}+{{ .Info.VersionMetadata }}{{- end }} 412 {{- if .Info.Release}}-{{ .Info.Release }}{{- end }} 413 {{- /* Optional fields */ -}} 414 {{- if .Info.IPK.ABIVersion}} 415 ABIVersion: {{.Info.IPK.ABIVersion}} 416 {{- end}} 417 {{- if .Info.IPK.Alternatives}} 418 Alternatives: {{ range $index, $element := .Info.IPK.Alternatives }}{{ if $index }}, {{end}}{{ $element.Priority }}:{{ $element.LinkName}}:{{ $element.Target}}{{- end }} 419 {{- end}} 420 {{- if .Info.IPK.AutoInstalled}} 421 Auto-Installed: yes 422 {{- end }} 423 {{- with .Info.Conflicts}} 424 Conflicts: {{join .}} 425 {{- end }} 426 {{- with .Info.Depends}} 427 Depends: {{join .}} 428 {{- end }} 429 {{- if .Info.IPK.Essential}} 430 Essential: yes 431 {{- end }} 432 {{- if .Info.Homepage}} 433 Homepage: {{.Info.Homepage}} 434 {{- end }} 435 {{- if .Info.License}} 436 License: {{.Info.License}} 437 {{- end }} 438 {{- if .InstalledSize }} 439 Installed-Size: {{.InstalledSize}} 440 {{- end }} 441 {{- with .Info.IPK.Predepends}} 442 Pre-Depends: {{join .}} 443 {{- end }} 444 {{- with nonEmpty .Info.Provides}} 445 Provides: {{join .}} 446 {{- end }} 447 {{- with .Info.Recommends}} 448 Recommends: {{join .}} 449 {{- end }} 450 {{- with .Info.Replaces}} 451 Replaces: {{join .}} 452 {{- end }} 453 {{- if .Info.Section}} 454 Section: {{.Info.Section}} 455 {{- end }} 456 {{- with .Info.Suggests}} 457 Suggests: {{join .}} 458 {{- end }} 459 {{- with .Info.IPK.Tags}} 460 Tags: {{join .}} 461 {{- end }} 462 {{- if .Info.Vendor}} 463 Vendor: {{.Info.Vendor}} 464 {{- end }} 465 {{- range $key, $value := .Info.IPK.Fields }} 466 {{- if $value }} 467 {{$key}}: {{$value}} 468 {{- end }} 469 {{- end }} 470 ` 471 472 type controlData struct { 473 Info *nfpm.Info 474 InstalledSize int64 475 } 476 477 func renderControl(w io.Writer, data controlData) error { 478 tmpl := template.New("control") 479 tmpl.Funcs(template.FuncMap{ 480 "join": func(strs []string) string { 481 return strings.Trim(strings.Join(strs, ", "), " ") 482 }, 483 "multiline": func(strs string) string { 484 var b strings.Builder 485 s := bufio.NewScanner(strings.NewReader(strings.TrimSpace(strs))) 486 s.Scan() 487 b.Write(bytes.TrimSpace(s.Bytes())) 488 for s.Scan() { 489 b.WriteString("\n ") 490 l := bytes.TrimSpace(s.Bytes()) 491 if len(l) == 0 { 492 b.WriteByte('.') 493 } else { 494 b.Write(l) 495 } 496 } 497 return b.String() 498 }, 499 "nonEmpty": func(strs []string) []string { 500 var result []string 501 for _, s := range strs { 502 s := strings.TrimSpace(s) 503 if s == "" { 504 continue 505 } 506 result = append(result, s) 507 } 508 return result 509 }, 510 }) 511 return template.Must(tmpl.Parse(controlTemplate)).Execute(w, data) 512 }