packaging/linux/deb/debroot.go (486 lines of code) (raw):
package deb
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"path/filepath"
"runtime/debug"
"strconv"
"strings"
"sync"
_ "embed"
"github.com/Azure/dalec"
"github.com/Azure/dalec/frontend/pkg/bkfs"
"github.com/moby/buildkit/client/llb"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
"github.com/pkg/errors"
)
const (
DebHelperCompat = "11"
customSystemdPostinstFile = "custom_systemd_postinst.sh.partial"
BinariesPath = "/usr/bin"
ConfigFilesPath = "/etc"
ManpagesPath = "/usr/share/doc/manpages"
HeadersPath = "/usr/include"
LicensesPath = "/usr/share/doc"
DocsPath = "/usr/share/doc"
LibsPath = "/usr/lib"
LibexecPath = "/usr/libexec"
DataDirsPath = "/usr/share"
)
//go:embed templates/patch-header.txt
var patchHeader []byte
//go:embed templates/debian_install_header.sh
var debianInstall []byte
// This creates a directory in the debian root directory for each patch, and copies the patch files into it.
// The format for each patch dir matches what would normally be under `debian/patches`, just that this is a separate dir for every source we are patching
// This is purely for documenting in the source package how patches are applied in a more readable way than the big merged patch file.
func sourcePatchesDir(sOpt dalec.SourceOpts, base llb.State, dir, name string, spec *dalec.Spec, opts ...llb.ConstraintsOpt) ([]llb.State, error) {
patchesPath := filepath.Join(dir, name)
base = base.
File(llb.Mkdir(patchesPath, 0o755), opts...)
var states []llb.State
seriesBuf := bytes.NewBuffer(nil)
for _, patch := range spec.Patches[name] {
src := spec.Sources[patch.Source]
copySrc := patch.Source
if patch.Path != "" {
src.Includes = append(src.Includes, patch.Path)
copySrc = patch.Path
}
st, err := src.AsState(patch.Source, sOpt, opts...)
if err != nil {
return nil, errors.Wrap(err, "error creating patch state")
}
st = base.File(llb.Copy(st, copySrc, filepath.Join(patchesPath, patch.Source)), opts...)
if _, err := seriesBuf.WriteString(name + "\n"); err != nil {
return nil, errors.Wrap(err, "error writing to series file")
}
states = append(states, st)
}
series := base.File(llb.Mkfile(filepath.Join(patchesPath, "series"), 0o640, seriesBuf.Bytes()), opts...)
return append(states, series), nil
}
type SourcePkgConfig struct {
// PrependPath is a list of paths to be prepended to the $PATH var in build
// scripts.
PrependPath []string
// AppendPath is a list of paths to be appended to the $PATH var in build
// scripts.
AppendPath []string
}
// Addpath creates a SourcePkgConfig where the first argument is sets
// [SourcePkgConfig.PrependPath] and the 2nd argument sets
// [SourcePkgConfig.AppendPath]
func AddPath(pre, post []string) SourcePkgConfig {
return SourcePkgConfig{
PrependPath: pre,
AppendPath: post,
}
}
// Debroot creates a debian root directory suitable for use with debbuild.
// This does not include sources in case you want to mount sources (instead of copying them) later.
//
// Set the `distroVersionID` argument to a value suitable for including in the
// .deb for storing the targeted distro+version in the deb.
// This is generally needed so that if a distro user upgrades from, for instance,
// debian 11 to debian 12, that the package built for debian 12 will be considered
// an upgrade even if it is technically the same underlying source.
// It may be left blank but is highly recommended to set this.
// Use [ReadDistroVersionID] to get a suitable value.
func Debroot(ctx context.Context, sOpt dalec.SourceOpts, spec *dalec.Spec, worker, in llb.State, target, dir, distroVersionID string, cfg SourcePkgConfig, opts ...llb.ConstraintsOpt) (llb.State, error) {
control, err := controlFile(spec, in, target, dir)
if err != nil {
return llb.Scratch(), errors.Wrap(err, "error generating control file")
}
rules, err := Rules(spec, in, dir, target)
if err != nil {
return llb.Scratch(), errors.Wrap(err, "error generating rules file")
}
changelog, err := Changelog(spec, in, target, dir, distroVersionID)
if err != nil {
return llb.Scratch(), errors.Wrap(err, "error generating changelog file")
}
if dir == "" {
dir = "debian"
}
base := llb.Scratch().File(llb.Mkdir(dir, 0o755), opts...)
installers := createInstallScripts(worker, spec, dir, target)
const (
sourceFormat = "3.0 (quilt)"
)
debian := base.
File(llb.Mkdir(filepath.Join(dir, "source"), 0o755), opts...).
File(llb.Mkfile(filepath.Join(dir, "source/format"), 0o640, []byte(sourceFormat)), opts...).
File(llb.Mkdir(filepath.Join(dir, "dalec"), 0o755), opts...).
File(llb.Mkfile(filepath.Join(dir, "source/include-binaries"), 0o640, append([]byte("dalec"), '\n')), opts...)
states := []llb.State{control, rules, changelog, debian}
states = append(states, installers...)
dalecDir := base.
File(llb.Mkdir(filepath.Join(dir, "dalec"), 0o755), opts...)
states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/build.sh"), 0o700, createBuildScript(spec, &cfg)), opts...))
states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/patch.sh"), 0o700, createPatchScript(spec, &cfg)), opts...))
states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/fix_generators.sh"), 0o700, fixupGenerators(spec, &cfg)), opts...))
states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/fix_perms.sh"), 0o700, fixupArtifactPerms(spec, target, &cfg)), opts...))
customEnable, err := customDHInstallSystemdPostinst(spec, target)
if err != nil {
return llb.Scratch(), err
}
if len(customEnable) > 0 {
// This is not meant to be executed on its own and will instead get added
// to a post inst file, so need to mark this as executable.
states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/"+customSystemdPostinstFile), 0o600, customEnable), opts...))
}
postinst := bytes.NewBuffer(nil)
artifacts := spec.GetArtifacts(target)
writeUsersPostInst(postinst, artifacts.Users)
writeGroupsPostInst(postinst, artifacts.Groups)
if postinst.Len() > 0 {
dt := []byte("#!/usr/bin/env sh\nset -e\n")
dt = append(dt, postinst.Bytes()...)
states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "postinst"), 0o700, dt), opts...))
}
patchDir := dalecDir.File(llb.Mkdir(filepath.Join(dir, "dalec/patches"), 0o755), opts...)
sorted := dalec.SortMapKeys(spec.Patches)
for _, name := range sorted {
pls, err := sourcePatchesDir(sOpt, patchDir, filepath.Join(dir, "dalec/patches"), name, spec, opts...)
if err != nil {
return llb.Scratch(), errors.Wrapf(err, "error creating patch directory for source %q", name)
}
states = append(states, pls...)
}
if len(artifacts.Links) > 0 {
buf := bytes.NewBuffer(nil)
for _, l := range artifacts.Links {
src := strings.TrimPrefix(l.Source, "/")
dst := strings.TrimPrefix(l.Dest, "/")
fmt.Fprintln(buf, src, dst)
}
states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, spec.Name+".links"), 0o644, buf.Bytes()), opts...))
}
return dalec.MergeAtPath(in, states, "/"), nil
}
func fixupArtifactPerms(spec *dalec.Spec, target string, cfg *SourcePkgConfig) []byte {
buf := bytes.NewBuffer(nil)
writeScriptHeader(buf, cfg)
basePath := filepath.Join("debian", spec.Name)
artifacts := spec.GetArtifacts(target)
checkAndWritePerms := func(artifacts map[string]dalec.ArtifactConfig, dir string) {
if artifacts == nil {
return
}
sorted := dalec.SortMapKeys(artifacts)
for _, key := range sorted {
cfg := artifacts[key]
resolvedName := cfg.ResolveName(key)
p := filepath.Join(basePath, dir, resolvedName)
if cfg.Permissions.Perm() != 0 {
fmt.Fprintf(buf, "chmod %o %q\n", cfg.Permissions.Perm(), p)
continue
}
// Debian does not keep original permissions for files, so we check if artifact matches a source name
// and if so, we apply the source permissions for inline sources.
srcKey, subpath, _ := strings.Cut(key, "/")
src, ok := spec.Sources[srcKey]
if !ok || src.Inline == nil {
continue
}
if src.Inline.File != nil && src.Inline.File.Permissions.Perm() != 0 {
fmt.Fprintf(buf, "chmod %o %q\n", src.Inline.File.Permissions.Perm(), p)
continue
}
if src.Inline.Dir == nil {
continue
}
if subpath == "" {
if src.Inline.Dir.Permissions.Perm() != 0 {
fmt.Fprintf(buf, "chmod %o %q\n", src.Inline.Dir.Permissions.Perm(), p)
}
continue
}
if f, ok := src.Inline.Dir.Files[subpath]; ok && f.Permissions.Perm() != 0 {
fmt.Fprintf(buf, "chmod %o %q\n", f.Permissions.Perm(), p)
}
}
}
checkAndWritePerms(artifacts.Binaries, BinariesPath)
checkAndWritePerms(artifacts.ConfigFiles, ConfigFilesPath)
checkAndWritePerms(artifacts.Manpages, filepath.Join(ManpagesPath, spec.Name))
checkAndWritePerms(artifacts.Headers, HeadersPath)
checkAndWritePerms(artifacts.Licenses, filepath.Join(LicensesPath, spec.Name))
checkAndWritePerms(artifacts.Docs, filepath.Join(DocsPath, spec.Name))
checkAndWritePerms(artifacts.Libs, filepath.Join(LibsPath))
checkAndWritePerms(artifacts.Libexec, LibexecPath)
checkAndWritePerms(artifacts.DataDirs, DataDirsPath)
if artifacts.Directories != nil {
sorted := dalec.SortMapKeys(artifacts.Directories.GetConfig())
for _, name := range sorted {
cfg := artifacts.Directories.Config[name]
if cfg.Mode.Perm() != 0 {
p := filepath.Join(basePath, "/etc", name)
fmt.Fprintf(buf, "chmod %o %q\n", cfg.Mode.Perm(), p)
}
}
sorted = dalec.SortMapKeys(artifacts.Directories.GetState())
for _, name := range sorted {
cfg := artifacts.Directories.State[name]
if cfg.Mode.Perm() != 0 {
p := filepath.Join(basePath, "/var/lib", name)
fmt.Fprintf(buf, "chmod %o %q\n", cfg.Mode.Perm(), p)
}
}
}
return buf.Bytes()
}
// For debian sources
// This is called from `debian/rules` after the source tarball has been extracted.
func fixupGenerators(spec *dalec.Spec, cfg *SourcePkgConfig) []byte {
buf := bytes.NewBuffer(nil)
writeScriptHeader(buf, cfg)
if spec.HasGomods() {
// Older go versions did not have support for the `GOMODCACHE` var
// This is a hack to try and make the build work by linking the go modules
// we've already fetched into to module dir under $GOPATH
// The default GOMODCACHE value is ${GOPATH}/pkg/mod.
fmt.Fprintf(buf, `test -n "$(go env GOMODCACHE)" || (GOPATH="$(go env GOPATH)"; mkdir -p "${GOPATH}/pkg" && ln -s "$(pwd)/%s" "${GOPATH}/pkg/mod")`, gomodsName)
// Above command does not have a newline due to quoting issues, so add that here.
fmt.Fprint(buf, "\n")
}
return buf.Bytes()
}
func setupPathVar(pre, post []string) string {
if len(pre) == 0 && len(post) == 0 {
return ""
}
full := append(pre, "$PATH")
full = append(full, post...)
return strings.Join(full, ":")
}
func writeScriptHeader(buf io.Writer, cfg *SourcePkgConfig) {
fmt.Fprintln(buf, "#!/usr/bin/env sh")
fmt.Fprintln(buf)
fmt.Fprintln(buf, "set -ex")
if cfg != nil {
if pathVar := setupPathVar(cfg.PrependPath, cfg.AppendPath); pathVar != "" {
fmt.Fprintln(buf, "export PATH="+pathVar)
}
}
}
func createPatchScript(spec *dalec.Spec, cfg *SourcePkgConfig) []byte {
buf := bytes.NewBuffer(nil)
writeScriptHeader(buf, cfg)
for name, patches := range spec.Patches {
for _, patch := range patches {
p := filepath.Join("${DEBIAN_DIR:=debian}/dalec/patches", name, patch.Source)
fmt.Fprintf(buf, "patch -d %q -p%d -s < %q\n", name, *patch.Strip, p)
}
}
return buf.Bytes()
}
func createBuildScript(spec *dalec.Spec, cfg *SourcePkgConfig) []byte {
buf := bytes.NewBuffer(nil)
writeScriptHeader(buf, cfg)
sorted := dalec.SortMapKeys(spec.Build.Env)
for _, k := range sorted {
v := spec.Build.Env[k]
fmt.Fprintf(buf, "export %q=%q\n", k, v)
}
for _, step := range spec.Build.Steps {
fmt.Fprintln(buf)
fmt.Fprintln(buf, "(")
sorted := dalec.SortMapKeys(step.Env)
for _, k := range sorted {
v := step.Env[k]
fmt.Fprintf(buf, " export %q=%q\n", k, v)
}
fmt.Fprintln(buf, step.Command)
fmt.Fprintln(buf, ")")
}
return buf.Bytes()
}
func createInstallScripts(worker llb.State, spec *dalec.Spec, dir, target string) []llb.State {
artifacts := spec.GetArtifacts(target)
states := make([]llb.State, 1)
base := llb.Scratch().File(llb.Mkdir(dir, 0o755, llb.WithParents(true)))
installBuf := bytes.NewBuffer(nil)
writeInstallHeader := sync.OnceFunc(func() {
fmt.Fprintln(installBuf, string(debianInstall))
})
writeInstall := func(src, dir, name string) {
// This is wrapped in a sync.OnceFunc so that this only has an effect the
// first time it is called.
writeInstallHeader()
name = strings.TrimSuffix(name, "*")
dest := filepath.Join("debian", spec.Name, dir, name)
fmt.Fprintln(installBuf, "do_install", filepath.Dir(dest), dest, src)
}
if len(artifacts.Binaries) > 0 {
sorted := dalec.SortMapKeys(artifacts.Binaries)
for _, key := range sorted {
cfg := artifacts.Binaries[key]
writeInstall(key, filepath.Join(BinariesPath, cfg.SubPath), cfg.ResolveName(key))
}
}
if len(artifacts.ConfigFiles) > 0 {
sorted := dalec.SortMapKeys(artifacts.ConfigFiles)
for _, p := range sorted {
cfg := artifacts.ConfigFiles[p]
dir := filepath.Join(ConfigFilesPath, cfg.SubPath)
name := cfg.ResolveName(p)
writeInstall(p, dir, name)
}
}
if len(artifacts.Manpages) > 0 {
buf := bytes.NewBuffer(nil)
sorted := dalec.SortMapKeys(artifacts.Manpages)
for _, key := range sorted {
cfg := artifacts.Manpages[key]
if cfg.Name != "" || (cfg.SubPath != "" && cfg.SubPath != filepath.Base(filepath.Dir(key))) {
resolved := cfg.ResolveName(key)
writeInstall(key, filepath.Join(ManpagesPath, spec.Name, cfg.SubPath), resolved)
continue
}
fmt.Fprintln(buf, key)
}
if buf.Len() > 0 {
states = append(states, base.File(llb.Mkfile(filepath.Join(dir, spec.Name+".manpages"), 0o640, buf.Bytes())))
}
}
if artifacts.Directories != nil {
buf := bytes.NewBuffer(nil)
sorted := dalec.SortMapKeys(artifacts.Directories.Config)
for _, name := range sorted {
fmt.Fprintln(buf, filepath.Join("/etc", name))
}
sorted = dalec.SortMapKeys(artifacts.Directories.State)
for _, name := range sorted {
fmt.Fprintln(buf, filepath.Join("/var/lib", name))
}
states = append(states, base.File(llb.Mkfile(filepath.Join(dir, spec.Name+".dirs"), 0o640, buf.Bytes())))
}
if len(artifacts.Docs) > 0 || len(artifacts.Licenses) > 0 {
buf := bytes.NewBuffer(nil)
sorted := dalec.SortMapKeys(artifacts.Docs)
for _, key := range sorted {
cfg := artifacts.Docs[key]
resolved := cfg.ResolveName(key)
if resolved != key || cfg.SubPath != "" {
writeInstall(key, filepath.Join(DocsPath, spec.Name, cfg.SubPath), resolved)
} else {
fmt.Fprintln(buf, key)
}
}
sorted = dalec.SortMapKeys(artifacts.Licenses)
for _, key := range sorted {
cfg := artifacts.Licenses[key]
resolved := cfg.ResolveName(key)
if resolved != key || cfg.SubPath != "" {
writeInstall(key, filepath.Join(LicensesPath, spec.Name, cfg.SubPath), resolved)
} else {
fmt.Fprintln(buf, key)
}
}
if buf.Len() > 0 {
states = append(states, base.File(llb.Mkfile(filepath.Join(dir, spec.Name+".docs"), 0o640, buf.Bytes())))
}
}
if len(artifacts.Headers) > 0 {
sorted := dalec.SortMapKeys(artifacts.Headers)
for _, key := range sorted {
cfg := artifacts.Headers[key]
resolved := cfg.ResolveName(key)
writeInstall(key, filepath.Join(HeadersPath, cfg.SubPath), resolved)
}
}
if units := artifacts.Systemd.GetUnits(); len(units) > 0 {
// deb-systemd will look for service files in DEBIAN/<package-name>[.<service-name>].<unit-type>
// To handle this we'll create symlinks to the actual unit files in the source.
// https://manpages.debian.org/testing/debhelper/dh_installsystemd.1.en.html#FILES
// Maps the base name of a unit, e.g. "foo.service" -> foo, to the list of
// units that fall under that basename
// (e.g. "foo.socket" and "foo.service")
// We need to track this in cases where some units under a base are
// enabled and some are not since dh_installsystemd does not support this
// directly.
sorted := dalec.SortMapKeys(units)
for _, key := range sorted {
cfg := units[key]
name, suffix := cfg.SplitName(key)
if name != spec.Name {
name = spec.Name + "." + name
}
name = name + "." + suffix
// Unforutnately there is not currently any way to create a symlink
// directory with llb, so we need to use the worker to create the
// symlink for us.
st := worker.Run(
llb.Dir(filepath.Join("/tmp/work", dir)),
dalec.ShArgs("ln -s ../"+key+" "+name),
).AddMount("/tmp/work", llb.Scratch())
states = append(states, st)
}
}
if dropins := artifacts.Systemd.GetDropins(); len(dropins) > 0 {
sorted := dalec.SortMapKeys(dropins)
for _, key := range sorted {
cfg := dropins[key]
cfgA := cfg.Artifact()
name := cfgA.ResolveName(key)
writeInstall(key, filepath.Join("/lib/systemd/system", cfg.Unit+".d"), name)
}
}
if len(artifacts.DataDirs) > 0 {
sorted := dalec.SortMapKeys(artifacts.DataDirs)
for _, key := range sorted {
cfg := artifacts.DataDirs[key]
resolved := cfg.ResolveName(key)
writeInstall(key, filepath.Join(DataDirsPath, cfg.SubPath), resolved)
}
}
if len(artifacts.Libexec) > 0 {
sorted := dalec.SortMapKeys(artifacts.Libexec)
for _, key := range sorted {
cfg := artifacts.Libexec[key]
resolved := cfg.ResolveName(key)
targetDir := filepath.Join(LibexecPath, cfg.SubPath)
writeInstall(key, targetDir, resolved)
}
}
if len(artifacts.Libs) > 0 {
sorted := dalec.SortMapKeys(artifacts.Libs)
for _, key := range sorted {
cfg := artifacts.Libs[key]
resolved := cfg.ResolveName(key)
writeInstall(key, filepath.Join(LibsPath, cfg.SubPath), resolved)
}
}
if installBuf.Len() > 0 {
states = append(states, base.File(llb.Mkfile(filepath.Join(dir, spec.Name+".install"), 0o700, installBuf.Bytes())))
}
return states
}
func controlFile(spec *dalec.Spec, in llb.State, target, dir string) (llb.State, error) {
buf := bytes.NewBuffer(nil)
info, _ := debug.ReadBuildInfo()
buf.WriteString("# Automatically generated by " + info.Main.Path + "\n")
buf.WriteString("\n")
if dir == "" {
dir = "debian"
}
if err := WriteControl(spec, target, buf); err != nil {
return llb.Scratch(), err
}
return in.
File(llb.Mkdir(dir, 0o755, llb.WithParents(true))).
File(llb.Mkfile(filepath.Join(dir, "control"), 0o640, buf.Bytes())),
nil
}
// ReadDistroVersionID returns a string concatenating the values of ID and
// VERSION_ID from /etc/os-release from the provided state.
func ReadDistroVersionID(ctx context.Context, client gwclient.Client, st llb.State) (string, error) {
rootfs, err := bkfs.FromState(ctx, &st, client)
if err != nil {
return "", err
}
f, err := rootfs.Open("etc/os-release")
if err != nil {
return "", err
}
defer f.Close()
scanner := bufio.NewScanner(f)
var (
id string
version string
)
for scanner.Scan() {
k, v, ok := strings.Cut(scanner.Text(), "=")
if !ok {
continue
}
switch k {
case "ID":
id = unquote(v)
case "VERSION_ID":
version = unquote(v)
}
if id != "" && version != "" {
break
}
}
if scanner.Err() != nil {
return "", err
}
if id == "" || version == "" {
return "", errors.New("could not determine distro or version ID")
}
return id + version, nil
}
func unquote(v string) string {
if updated, err := strconv.Unquote(v); err == nil {
return updated
}
return v
}
func writeUsersPostInst(w io.Writer, users []dalec.AddUserConfig) {
for _, u := range users {
fmt.Fprintf(w, "getent passwd %s >/dev/null || useradd %s\n", u.Name, u.Name)
}
}
func writeGroupsPostInst(w io.Writer, groups []dalec.AddGroupConfig) {
for _, g := range groups {
fmt.Fprintf(w, "getent group %s >/dev/null || groupadd --system %s\n", g.Name, g.Name)
}
}