github.com/goreleaser/nfpm/v2@v2.44.0/apk/apk.go (about) 1 /* 2 Copyright 2019 Torsten Curdt 3 4 Permission is hereby granted, free of charge, to any person obtaining a copy 5 of this software and associated documentation files (the "Software"), to deal 6 in the Software without restriction, including without limitation the rights 7 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 copies of the Software, and to permit persons to whom the Software is 9 furnished to do so, subject to the following conditions: 10 11 The above copyright notice and this permission notice shall be included in all 12 copies or substantial portions of the Software. 13 14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 SOFTWARE. 21 */ 22 23 // Package apk implements nfpm.Packager providing .apk bindings. 24 package apk 25 26 // Initial implementation from https://gist.github.com/tcurdt/512beaac7e9c12dcf5b6b7603b09d0d8 27 28 import ( 29 "archive/tar" 30 "bufio" 31 "bytes" 32 "crypto/sha1" 33 "crypto/sha256" 34 "encoding/hex" 35 "errors" 36 "fmt" 37 "hash" 38 "io" 39 "net/mail" 40 "os" 41 "strings" 42 "sync/atomic" 43 "text/template" 44 "time" 45 46 "github.com/goreleaser/nfpm/v2" 47 "github.com/goreleaser/nfpm/v2/files" 48 "github.com/goreleaser/nfpm/v2/internal/maps" 49 "github.com/goreleaser/nfpm/v2/internal/sign" 50 gzip "github.com/klauspost/pgzip" 51 ) 52 53 const packagerName = "apk" 54 55 // nolint: gochecknoinits 56 func init() { 57 nfpm.RegisterPackager(packagerName, Default) 58 } 59 60 // https://wiki.alpinelinux.org/wiki/Architecture 61 // nolint: gochecknoglobals 62 var archToAlpine = map[string]string{ 63 "386": "x86", 64 "amd64": "x86_64", 65 "arm64": "aarch64", 66 "arm6": "armhf", 67 "arm7": "armv7", 68 "ppc64le": "ppc64le", 69 "s390": "s390x", 70 } 71 72 func ensureValidArch(info *nfpm.Info) *nfpm.Info { 73 if info.APK.Arch != "" { 74 info.Arch = info.APK.Arch 75 } else if arch, ok := archToAlpine[info.Arch]; ok { 76 info.Arch = arch 77 } 78 79 return info 80 } 81 82 // Default apk packager. 83 // nolint: gochecknoglobals 84 var Default = &Apk{} 85 86 // Apk is an apk packager implementation. 87 type Apk struct{} 88 89 func (a *Apk) ConventionalFileName(info *nfpm.Info) string { 90 info = ensureValidArch(info) 91 version := pkgver(info) 92 return fmt.Sprintf("%s_%s_%s.apk", info.Name, version, info.Arch) 93 } 94 95 // ConventionalExtension returns the file name conventionally used for Apk packages 96 func (*Apk) ConventionalExtension() string { 97 return ".apk" 98 } 99 100 // Package writes a new apk package to the given writer using the given info. 101 func (*Apk) Package(info *nfpm.Info, apk io.Writer) (err error) { 102 if info.Platform != "linux" { 103 return fmt.Errorf("invalid platform: %s", info.Platform) 104 } 105 info = ensureValidArch(info) 106 107 if err := nfpm.PrepareForPackager(info, packagerName); err != nil { 108 return err 109 } 110 111 var bufData bytes.Buffer 112 113 size := int64(0) 114 // create the data tgz 115 dataDigest, err := createData(&bufData, info, &size) 116 if err != nil { 117 return err 118 } 119 120 // create the control tgz 121 var bufControl bytes.Buffer 122 controlDigest, err := createControl(&bufControl, info, size, dataDigest) 123 if err != nil { 124 return err 125 } 126 127 if info.APK.Signature.KeyFile == "" && info.APK.Signature.SignFn == nil { 128 return combineToApk(apk, &bufControl, &bufData) 129 } 130 131 // create the signature tgz 132 var bufSignature bytes.Buffer 133 if err = createSignature(&bufSignature, info, controlDigest); err != nil { 134 return err 135 } 136 137 return combineToApk(apk, &bufSignature, &bufControl, &bufData) 138 } 139 140 type writerCounter struct { 141 io.Writer 142 count uint64 143 writer io.Writer 144 } 145 146 func newWriterCounter(w io.Writer) *writerCounter { 147 return &writerCounter{ 148 writer: w, 149 } 150 } 151 152 func (counter *writerCounter) Write(buf []byte) (int, error) { 153 n, err := counter.writer.Write(buf) 154 atomic.AddUint64(&counter.count, uint64(n)) 155 return n, err 156 } 157 158 func (counter *writerCounter) Count() uint64 { 159 return atomic.LoadUint64(&counter.count) 160 } 161 162 func writeFile(tw *tar.Writer, header *tar.Header, file io.Reader) error { 163 header.Format = tar.FormatUSTAR 164 header.ChangeTime = time.Time{} 165 header.AccessTime = time.Time{} 166 167 err := tw.WriteHeader(header) 168 if err != nil { 169 return err 170 } 171 172 _, err = io.Copy(tw, file) 173 return err 174 } 175 176 type tarKind int 177 178 const ( 179 tarFull tarKind = iota 180 tarCut 181 ) 182 183 func writeTgz(w io.Writer, kind tarKind, builder func(tw *tar.Writer) error, digest hash.Hash) ([]byte, error) { 184 mw := io.MultiWriter(digest, w) 185 gw := gzip.NewWriter(mw) 186 cw := newWriterCounter(gw) 187 bw := bufio.NewWriterSize(cw, 4096) 188 tw := tar.NewWriter(bw) 189 190 err := builder(tw) 191 if err != nil { 192 return nil, err 193 } 194 195 // handle the cut vs full tars 196 // TODO: document this better, why do we need to call bw.Flush twice if it is a full tar vs the cut tar? 197 if err = bw.Flush(); err != nil { 198 return nil, err 199 } 200 if err = tw.Close(); err != nil { 201 return nil, err 202 } 203 if kind == tarFull { 204 if err = bw.Flush(); err != nil { 205 return nil, err 206 } 207 } 208 209 size := cw.Count() 210 alignedSize := (size + 511) & ^uint64(511) 211 212 increase := alignedSize - size 213 if increase > 0 { 214 b := make([]byte, increase) 215 _, err = cw.Write(b) 216 if err != nil { 217 return nil, err 218 } 219 } 220 221 if err = gw.Close(); err != nil { 222 return nil, err 223 } 224 225 return digest.Sum(nil), nil 226 } 227 228 func createData(dataTgz io.Writer, info *nfpm.Info, sizep *int64) ([]byte, error) { 229 builderData := createBuilderData(info, sizep) 230 dataDigest, err := writeTgz(dataTgz, tarFull, builderData, sha256.New()) 231 if err != nil { 232 return nil, err 233 } 234 return dataDigest, nil 235 } 236 237 func createControl(controlTgz io.Writer, info *nfpm.Info, size int64, dataDigest []byte) ([]byte, error) { 238 builderControl := createBuilderControl(info, size, dataDigest) 239 controlDigest, err := writeTgz(controlTgz, tarCut, builderControl, sha1.New()) // nolint:gosec 240 if err != nil { 241 return nil, err 242 } 243 return controlDigest, nil 244 } 245 246 func createSignature(signatureTgz io.Writer, info *nfpm.Info, controlSHA1Digest []byte) error { 247 signatureBuilder := createSignatureBuilder(controlSHA1Digest, info) 248 // we don't actually need to produce a digest here, but writeTgz 249 // requires it so we just use SHA1 since it is already imported 250 _, err := writeTgz(signatureTgz, tarCut, signatureBuilder, sha1.New()) // nolint:gosec 251 if err != nil { 252 return &nfpm.ErrSigningFailure{Err: err} 253 } 254 255 return nil 256 } 257 258 var errNoKeyAddress = errors.New("key name not set and maintainer mail address empty") 259 260 func createSignatureBuilder(digest []byte, info *nfpm.Info) func(*tar.Writer) error { 261 return func(tw *tar.Writer) error { 262 var signature []byte 263 var err error 264 if signFn := info.APK.Signature.SignFn; signFn != nil { 265 signature, err = signFn(bytes.NewReader(digest)) 266 } else { 267 signature, err = sign.RSASignSHA1Digest(digest, 268 info.APK.Signature.KeyFile, info.APK.Signature.KeyPassphrase) 269 } 270 if err != nil { 271 return err 272 } 273 274 // needs to exist on the machine during installation: /etc/apk/keys/<keyname>.rsa.pub 275 keyname := info.APK.Signature.KeyName 276 if keyname == "" { 277 addr, err := mail.ParseAddress(info.Maintainer) 278 if err != nil { 279 return fmt.Errorf("key name not set and unable to parse maintainer mail address: %w", err) 280 } else if addr.Address == "" { 281 return errNoKeyAddress 282 } 283 284 keyname = addr.Address 285 } 286 if !strings.HasSuffix(keyname, ".rsa.pub") { 287 keyname += ".rsa.pub" 288 } 289 290 // In principle apk supports RSA signatures over SHA256/512 keys, but in 291 // practice verification works but installation segfaults. If this is 292 // fixed at some point we should also upgrade the hash. In this case, 293 // the file name will have to start with .SIGN.RSA256 or .SIGN.RSA512. 294 signHeader := &tar.Header{ 295 Name: fmt.Sprintf(".SIGN.RSA.%s", keyname), 296 Mode: 0o600, 297 Size: int64(len(signature)), 298 } 299 300 return writeFile(tw, signHeader, bytes.NewReader(signature)) 301 } 302 } 303 304 func combineToApk(target io.Writer, readers ...io.Reader) error { 305 for _, tgz := range readers { 306 if _, err := io.Copy(target, tgz); err != nil { 307 return err 308 } 309 } 310 return nil 311 } 312 313 func createBuilderControl(info *nfpm.Info, size int64, dataDigest []byte) func(tw *tar.Writer) error { 314 return func(tw *tar.Writer) error { 315 var infoBuf bytes.Buffer 316 if err := writeControl(&infoBuf, controlData{ 317 Info: info, 318 InstalledSize: size, 319 Datahash: hex.EncodeToString(dataDigest), 320 }); err != nil { 321 return err 322 } 323 infoContent := infoBuf.String() 324 325 infoHeader := &tar.Header{ 326 Name: ".PKGINFO", 327 Mode: 0o600, 328 Size: int64(len(infoContent)), 329 } 330 331 if err := writeFile(tw, infoHeader, strings.NewReader(infoContent)); err != nil { 332 return err 333 } 334 335 // NOTE: Apk scripts tend to follow the pattern: 336 // #!/bin/sh 337 // 338 // bin/echo 'running preinstall.sh' // do stuff here 339 // 340 // exit 0 341 scripts := map[string]string{ 342 ".pre-install": info.Scripts.PreInstall, 343 ".pre-upgrade": info.APK.Scripts.PreUpgrade, 344 ".post-install": info.Scripts.PostInstall, 345 ".post-upgrade": info.APK.Scripts.PostUpgrade, 346 ".pre-deinstall": info.Scripts.PreRemove, 347 ".post-deinstall": info.Scripts.PostRemove, 348 } 349 for _, name := range maps.Keys(scripts) { 350 path := scripts[name] 351 if path == "" { 352 continue 353 } 354 if err := newScriptInsideTarGz(tw, path, name); err != nil { 355 return err 356 } 357 } 358 359 return nil 360 } 361 } 362 363 func newScriptInsideTarGz(out *tar.Writer, path, dest string) error { 364 file, err := os.Stat(path) //nolint:gosec 365 if err != nil { 366 return err 367 } 368 content, err := os.ReadFile(path) 369 if err != nil { 370 return err 371 } 372 return newItemInsideTarGz(out, content, &tar.Header{ 373 Name: files.ToNixPath(dest), 374 Size: int64(len(content)), 375 Mode: 0o755, 376 ModTime: file.ModTime(), 377 Typeflag: tar.TypeReg, 378 }) 379 } 380 381 func newItemInsideTarGz(out *tar.Writer, content []byte, header *tar.Header) error { 382 header.Format = tar.FormatPAX 383 header.PAXRecords = make(map[string]string) 384 385 hasher := sha1.New() 386 _, err := hasher.Write(content) 387 if err != nil { 388 return fmt.Errorf("failed to hash content of file %s: %w", header.Name, err) 389 } 390 header.PAXRecords["APK-TOOLS.checksum.SHA1"] = fmt.Sprintf("%x", hasher.Sum(nil)) 391 if err := out.WriteHeader(header); err != nil { 392 return fmt.Errorf("cannot write header of %s file to apk: %w", header.Name, err) 393 } 394 if _, err := out.Write(content); err != nil { 395 return fmt.Errorf("cannot write %s file to apk: %w", header.Name, err) 396 } 397 return nil 398 } 399 400 func createBuilderData(info *nfpm.Info, sizep *int64) func(tw *tar.Writer) error { 401 return func(tw *tar.Writer) error { 402 return createFilesInsideTarGz(info, tw, sizep) 403 } 404 } 405 406 func createFilesInsideTarGz(info *nfpm.Info, tw *tar.Writer, sizep *int64) (err error) { 407 for _, file := range info.Contents { 408 file.Destination = files.AsRelativePath(file.Destination) 409 410 switch file.Type { 411 case files.TypeDir, files.TypeImplicitDir: 412 err = tw.WriteHeader(&tar.Header{ 413 Name: file.Destination, 414 Mode: int64(file.FileInfo.Mode), 415 Typeflag: tar.TypeDir, 416 Uname: file.FileInfo.Owner, 417 Gname: file.FileInfo.Group, 418 ModTime: file.FileInfo.MTime, 419 }) 420 case files.TypeSymlink: 421 err = newItemInsideTarGz(tw, []byte{}, &tar.Header{ 422 Name: file.Destination, 423 Linkname: file.Source, 424 Typeflag: tar.TypeSymlink, 425 ModTime: file.FileInfo.MTime, 426 }) 427 default: 428 err = copyToTarAndDigest(file, tw, sizep) 429 } 430 if err != nil { 431 return err 432 } 433 } 434 435 return nil 436 } 437 438 func copyToTarAndDigest(file *files.Content, tw *tar.Writer, sizep *int64) error { 439 contents, err := os.ReadFile(file.Source) 440 if err != nil { 441 return err 442 } 443 header, err := tar.FileInfoHeader(file, file.Source) 444 if err != nil { 445 return err 446 } 447 448 // tar.FileInfoHeader only uses file.Mode().Perm() which masks the mode with 449 // 0o777 which we don't want because we want to be able to set the suid bit. 450 header.Mode = int64(file.Mode()) 451 header.Name = files.AsRelativePath(file.Destination) 452 header.Uname = file.FileInfo.Owner 453 header.Gname = file.FileInfo.Group 454 if err = newItemInsideTarGz(tw, contents, header); err != nil { 455 return err 456 } 457 458 *sizep += file.Size() 459 return nil 460 } 461 462 // reference: https://wiki.adelielinux.org/wiki/APK_internals#.PKGINFO 463 const controlTemplate = ` 464 {{- /* Mandatory fields */ -}} 465 pkgname = {{.Info.Name}} 466 pkgver = {{ pkgver .Info }} 467 arch = {{.Info.Arch}} 468 size = {{.InstalledSize}} 469 pkgdesc = {{multiline .Info.Description}} 470 {{- if .Info.Homepage}} 471 url = {{.Info.Homepage}} 472 {{- end }} 473 {{- if .Info.Maintainer}} 474 maintainer = {{.Info.Maintainer}} 475 {{- end }} 476 {{- range $repl := .Info.Replaces}} 477 replaces = {{ $repl }} 478 {{- end }} 479 {{- range $prov := .Info.Provides}} 480 provides = {{ $prov }} 481 {{- end }} 482 {{- range $dep := .Info.Depends}} 483 depend = {{ $dep }} 484 {{- end }} 485 {{- if .Info.License}} 486 license = {{.Info.License}} 487 {{- end }} 488 datahash = {{.Datahash}} 489 ` 490 491 type controlData struct { 492 Info *nfpm.Info 493 InstalledSize int64 494 Datahash string 495 } 496 497 func writeControl(w io.Writer, data controlData) error { 498 tmpl := template.New("control") 499 tmpl.Funcs(template.FuncMap{ 500 "multiline": func(strs string) string { 501 ret := strings.ReplaceAll(strs, "\n", "\n ") 502 return strings.Trim(ret, " \n") 503 }, 504 "pkgver": pkgver, 505 }) 506 return template.Must(tmpl.Parse(controlTemplate)).Execute(w, data) 507 } 508 509 func pkgver(info *nfpm.Info) string { 510 version := info.Version 511 512 if info.Prerelease != "" { 513 version += "_" + info.Prerelease 514 } 515 516 if rel := info.Release; rel != "" { 517 if !strings.HasPrefix(rel, "r") { 518 rel = "r" + rel 519 } 520 version += "-" + rel 521 } 522 if meta := info.VersionMetadata; meta != "" { 523 if !strings.HasPrefix(meta, "p") && 524 !strings.HasPrefix(meta, "cvs") && 525 !strings.HasPrefix(meta, "svn") && 526 !strings.HasPrefix(meta, "git") && 527 !strings.HasPrefix(meta, "hg") { 528 meta = "p" + meta 529 } 530 version += "-" + meta 531 } 532 return version 533 }