packaging/linux/rpm/template.go (694 lines of code) (raw):

package rpm import ( "bufio" "fmt" "io" "path/filepath" "strconv" "strings" "sync" "text/template" "github.com/Azure/dalec" "golang.org/x/exp/maps" "golang.org/x/exp/slices" ) const ( gomodsName = "__gomods" cargohomeName = "__cargohome" buildScriptName = "build.sh" ) var specTmpl = template.Must(template.New("spec").Funcs(tmplFuncs).Parse(strings.TrimSpace(` {{.DisableStrip}} Name: {{.Name}} Version: {{.Version}} Release: {{.Release}}%{?dist} License: {{ .License }} Summary: {{ .Description }} {{ optionalField "URL" .Website -}} {{ optionalField "Vendor" .Vendor -}} {{ optionalField "Packager" .Packager -}} {{ if .NoArch }} BuildArch: noarch {{ end }} {{- .Sources -}} {{- .Conflicts -}} {{- .Provides -}} {{- .Replaces -}} {{- .Requires -}} {{- .Recommends -}} %description {{.Description}} {{ .PrepareSources -}} {{ .BuildSteps -}} {{ .Install -}} {{ .Post -}} {{ .PreUn -}} {{ .PostUn -}} {{ .Files -}} {{ .Changelog -}} `))) func optionalField(key, value string) string { if value == "" { return "" } return key + ": " + value + "\n" } var tmplFuncs = map[string]any{ "optionalField": optionalField, } type specWrapper struct { *dalec.Spec Target string } func (w *specWrapper) Changelog() (fmt.Stringer, error) { b := &strings.Builder{} if len(w.Spec.Changelog) == 0 { return b, nil } fmt.Fprintf(b, "%%changelog\n") for _, log := range w.Spec.Changelog { fmt.Fprintln(b, "* "+log.Date.Format("Mon Jan 2 2006")+" "+log.Author) for _, change := range log.Changes { fmt.Fprintln(b, "- "+change) } } b.WriteString("\n") return b, nil } func (w *specWrapper) Provides() fmt.Stringer { b := &strings.Builder{} ls := maps.Keys(w.Spec.Provides) slices.Sort(ls) for _, name := range ls { writeDep(b, "Provides", name, w.Spec.Provides[name]) } b.WriteString("\n") return b } func (w *specWrapper) Replaces() fmt.Stringer { b := &strings.Builder{} keys := dalec.SortMapKeys(w.Spec.Replaces) for _, name := range keys { writeDep(b, "Obsoletes", name, w.Spec.Replaces[name]) } return b } func getSystemdRequires(cfg *dalec.SystemdConfiguration) string { var requires, orderRequires string if cfg.IsEmpty() { return "" } enabledUnits := cfg.EnabledUnits() if len(enabledUnits) > 0 { // if we are enabling any units, we need to require systemd // specifically for %post requires += "Requires(post): systemd\n" orderRequires += "OrderWithRequires(post): systemd\n" } // in any case where we have units as artifacts, we must require systemd // for %preun and %postun, as we are using the rpm systemd macros // in those stages which depend on systemctl requires += `Requires(preun): systemd Requires(postun): systemd ` orderRequires += `OrderWithRequires(preun): systemd OrderWithRequires(postun): systemd ` return requires + orderRequires } func getUserPostRequires(users []dalec.AddUserConfig, groups []dalec.AddGroupConfig) string { var out string if len(users) > 0 { out += "Requires(post): /usr/sbin/adduser, /usr/bin/getent\n" } if len(groups) > 0 { out += "Requires(post): /usr/sbin/groupadd, /usr/bin/getent\n" } return out } func (w *specWrapper) Requires() fmt.Stringer { b := &strings.Builder{} artifacts := w.Spec.GetArtifacts(w.Target) // first write post requires for systemd and user/group creation // as these do not come from dependencies in the spec // NOTE: This is a bit janky since different distributions may have different // package names... something to consider as we expand functionality. b.WriteString(getSystemdRequires(artifacts.Systemd)) b.WriteString(getUserPostRequires(artifacts.Users, artifacts.Groups)) deps := w.GetPackageDeps(w.Target) if deps == nil { return b } buildKeys := dalec.SortMapKeys(deps.Build) for _, name := range buildKeys { constraints := deps.Build[name] writeDep(b, "BuildRequires", name, constraints) } if len(deps.Build) > 0 && len(deps.Runtime) > 0 { b.WriteString("\n") } runtimeKeys := dalec.SortMapKeys(deps.Runtime) for _, name := range runtimeKeys { constraints := deps.Runtime[name] // TODO: consider if it makes sense to support sources satisfying runtime deps writeDep(b, "Requires", name, constraints) } b.WriteString("\n") return b } func (w *specWrapper) Recommends() fmt.Stringer { b := &strings.Builder{} deps := w.GetPackageDeps(w.Target) if deps == nil { return b } if len(deps.Recommends) == 0 { return b } keys := dalec.SortMapKeys(deps.Recommends) for _, name := range keys { constraints := deps.Recommends[name] writeDep(b, "Recommends", name, constraints) } b.WriteString("\n") return b } // NOTE: This is very basic and does not handle things like grouped constraints // Given this is just trying to shim things to allow either the rpm format or the deb format // in its basic form, this is sufficient for now. func formatVersionConstraint(v string) string { prefix, suffix, ok := strings.Cut(v, " ") if !ok { if len(prefix) >= 1 { _, err := strconv.Atoi(prefix[:1]) if err == nil { // This is just a version number, assume it should use the equal symbol return "== " + v } } return v } switch prefix { case "<<": return "< " + suffix case ">>": return "> " + suffix case "=": return "== " + suffix default: return v } } func writeDep(b *strings.Builder, kind, name string, constraints dalec.PackageConstraints) { do := func() { if len(constraints.Version) == 0 { fmt.Fprintf(b, "%s: %s\n", kind, name) return } for _, c := range constraints.Version { fmt.Fprintf(b, "%s: %s %s\n", kind, name, formatVersionConstraint(c)) } } if len(constraints.Arch) == 0 { do() return } for _, arch := range constraints.Arch { fmt.Fprintf(b, "%%ifarch %s\n", arch) do() fmt.Fprintf(b, "%%endif\n") } } func (w *specWrapper) Conflicts() string { b := &strings.Builder{} keys := dalec.SortMapKeys(w.Spec.Conflicts) for _, name := range keys { constraints := w.Spec.Conflicts[name] writeDep(b, "Conflicts", name, constraints) } b.WriteString("\n") return b.String() } func (w *specWrapper) Sources() (fmt.Stringer, error) { b := &strings.Builder{} // Sort keys for consistent output keys := dalec.SortMapKeys(w.Spec.Sources) for idx, name := range keys { src := w.Spec.Sources[name] ref := name isDir := dalec.SourceIsDir(src) if isDir { ref += ".tar.gz" } doc, err := src.Doc(name) if err != nil { return nil, fmt.Errorf("error getting doc for source %s: %w", name, err) } scanner := bufio.NewScanner(doc) for scanner.Scan() { fmt.Fprintf(b, "# %s\n", scanner.Text()) } if scanner.Err() != nil { return nil, scanner.Err() } fmt.Fprintf(b, "Source%d: %s\n", idx, ref) } sourceIdx := len(keys) if w.Spec.HasGomods() { fmt.Fprintf(b, "Source%d: %s.tar.gz\n", sourceIdx, gomodsName) sourceIdx += 1 } if w.Spec.HasCargohomes() { fmt.Fprintf(b, "Source%d: %s.tar.gz\n", sourceIdx, cargohomeName) sourceIdx += 1 } if len(w.Spec.Build.Steps) > 0 { fmt.Fprintf(b, "Source%d: %s\n", sourceIdx, buildScriptName) } if len(keys) > 0 { b.WriteString("\n") } return b, nil } func (w *specWrapper) Release() string { if w.Spec.Revision == "" { return "1" } return w.Spec.Revision } func (w *specWrapper) PrepareSources() (fmt.Stringer, error) { b := &strings.Builder{} if len(w.Spec.Sources) == 0 { return b, nil } fmt.Fprintf(b, "%%prep\n") patches := make(map[string]bool) for _, v := range w.Spec.Patches { for _, p := range v { patches[p.Source] = true } } // Sort keys for consistent output keys := dalec.SortMapKeys(w.Spec.Sources) prepareGomods := sync.OnceFunc(func() { if !w.Spec.HasGomods() { return } fmt.Fprintf(b, "mkdir -p \"%%{_builddir}/%s\"\n", gomodsName) fmt.Fprintf(b, "tar -C \"%%{_builddir}/%s\" -xzf \"%%{_sourcedir}/%s.tar.gz\"\n", gomodsName, gomodsName) }) prepareCargohomes := sync.OnceFunc(func() { if !w.Spec.HasCargohomes() { return } fmt.Fprintf(b, "mkdir -p \"%%{_builddir}/%s\"\n", cargohomeName) fmt.Fprintf(b, "tar -C \"%%{_builddir}/%s\" -xzf \"%%{_sourcedir}/%s.tar.gz\"\n", cargohomeName, cargohomeName) }) // Extract all the sources from the rpm source dir for _, key := range keys { if !dalec.SourceIsDir(w.Spec.Sources[key]) { // This is a file, nothing to extract, but we need to put it into place // in the rpm build dir fmt.Fprintf(b, "cp -a \"%%{_sourcedir}/%s\" .\n", key) continue } // This is a directory source so it needs to be untarred into the rpm build dir. fmt.Fprintf(b, "mkdir -p \"%%{_builddir}/%s\"\n", key) fmt.Fprintf(b, "tar -C \"%%{_builddir}/%s\" -xzf \"%%{_sourcedir}/%s.tar.gz\"\n", key, key) } prepareGomods() prepareCargohomes() // Apply patches to all sources. // Note: These are applied based on the key sorting algorithm (lexicographic). // Using one patch to patch another patch is not supported, except that it may // occur if they happen to be sorted lexicographically. patchKeys := dalec.SortMapKeys(w.Spec.Patches) for _, key := range patchKeys { for _, patch := range w.Spec.Patches[key] { fmt.Fprintf(b, "patch -d %q -p%d -s --input \"%%{_builddir}/%s\"\n", key, *patch.Strip, filepath.Join(patch.Source, patch.Path)) } } if len(keys) > 0 { b.WriteString("\n") } return b, nil } func writeStep(b *strings.Builder, step dalec.BuildStep) { envKeys := dalec.SortMapKeys(step.Env) // Wrap commands in a subshell so any environment variables that are set // will be available to every command in the BuildStep fmt.Fprintln(b, "(") // begin subshell for _, k := range envKeys { fmt.Fprintf(b, "export %s=\"%s\"\n", k, step.Env[k]) } fmt.Fprintf(b, "%s", step.Command) fmt.Fprintln(b, ")") // end subshell } func (w *specWrapper) BuildSteps() fmt.Stringer { b := &strings.Builder{} if len(w.Spec.Build.Steps) == 0 { return b } fmt.Fprintf(b, "%%build\n") fmt.Fprintf(b, "%%{_sourcedir}/%s\n", buildScriptName) b.WriteString("\n") return b } func (w *specWrapper) PreUn() fmt.Stringer { b := &strings.Builder{} artifacts := w.GetArtifacts(w.Target) if artifacts.Systemd.IsEmpty() { return b } b.WriteString("%preun\n") keys := dalec.SortMapKeys(artifacts.Systemd.Units) for _, servicePath := range keys { serviceName := filepath.Base(servicePath) fmt.Fprintf(b, "%%systemd_preun %s\n", serviceName) } b.WriteString("\n") return b } func systemdPostScript(unitName string, cfg dalec.SystemdUnitConfig) string { // if service isn't explicitly specified as enabled in the spec, // then we don't need to do anything in the post script if !cfg.Enable { return "" } // should be equivalent to the systemd_post scriptlet in the rpm spec, // but without the use of a .preset file return fmt.Sprintf(` if [ $1 -eq 1 ]; then # initial installation systemctl enable %s fi `, unitName) } func (w *specWrapper) Post() fmt.Stringer { b := &strings.Builder{} systemd := w.postSystemd() users := w.postUsers() groups := w.postGroups() if systemd == "" && users == "" && groups == "" { return b } b.WriteString("%post\n") if systemd != "" { b.WriteString(systemd) } if users != "" { b.WriteString(users) } if groups != "" { b.WriteString(groups) } b.WriteString("\n") return b } func (w *specWrapper) postUsers() string { artifacts := w.Spec.GetArtifacts(w.Target) if len(artifacts.Users) == 0 { return "" } b := &strings.Builder{} for _, user := range artifacts.Users { fmt.Fprintf(b, "getent passwd %s >/dev/null || adduser %s\n", user.Name, user.Name) } return b.String() } func (w *specWrapper) postGroups() string { artifacts := w.Spec.GetArtifacts(w.Target) if len(artifacts.Groups) == 0 { return "" } b := &strings.Builder{} for _, group := range artifacts.Groups { fmt.Fprintf(b, "getent group %s >/dev/null || groupadd --system %s\n", group.Name, group.Name) } return b.String() } func (w *specWrapper) postSystemd() string { artifacts := w.Spec.GetArtifacts(w.Target) if artifacts.Systemd.IsEmpty() { return "" } enabledUnits := artifacts.Systemd.EnabledUnits() if len(enabledUnits) == 0 { // if we have no enabled units, we don't need to do anything systemd related // in the post script. In this case, we shouldn't emit '%post' // as this eliminates the need for extra dependencies in the target container return "" } b := &strings.Builder{} keys := dalec.SortMapKeys(enabledUnits) for _, servicePath := range keys { unitConf := artifacts.Systemd.Units[servicePath] artifact := unitConf.Artifact() b.WriteString( systemdPostScript(artifact.ResolveName(servicePath), unitConf), ) } return b.String() } func (w *specWrapper) PostUn() fmt.Stringer { b := &strings.Builder{} artifacts := w.GetArtifacts(w.Target) if artifacts.Systemd.IsEmpty() { return b } b.WriteString("%postun\n") keys := dalec.SortMapKeys(artifacts.Systemd.Units) for _, servicePath := range keys { cfg := artifacts.Systemd.Units[servicePath] a := cfg.Artifact() serviceName := a.ResolveName(servicePath) fmt.Fprintf(b, "%%systemd_postun %s\n", serviceName) } return b } func (w *specWrapper) Install() fmt.Stringer { b := &strings.Builder{} fmt.Fprintln(b, "%install") artifacts := w.Spec.GetArtifacts(w.Target) copyArtifact := func(root, p string, cfg *dalec.ArtifactConfig) { if cfg == nil { return } targetDir := filepath.Join(root, cfg.SubPath) fmt.Fprintln(b, "mkdir -p", targetDir) var targetPath string file := cfg.ResolveName(p) if !strings.Contains(file, "*") { targetPath = filepath.Join(targetDir, file) } else { targetPath = targetDir + "/" } fmt.Fprintln(b, "cp -r", p, targetPath) if cfg.Permissions.Perm() != 0 { fmt.Fprintf(b, "chmod %o %s\n", cfg.Permissions, targetPath) } } if len(artifacts.Binaries) > 0 { binKeys := dalec.SortMapKeys(artifacts.Binaries) for _, p := range binKeys { cfg := artifacts.Binaries[p] copyArtifact(`%{buildroot}/%{_bindir}`, p, &cfg) } } if len(artifacts.Manpages) > 0 { manKeys := dalec.SortMapKeys(artifacts.Manpages) for _, p := range manKeys { cfg := artifacts.Manpages[p] copyArtifact(`%{buildroot}/%{_mandir}`, p, &cfg) } } createArtifactDir := func(root, p string, cfg dalec.ArtifactDirConfig) { dir := filepath.Join(root, p) mkdirCmd := "mkdir" perms := cfg.Mode.Perm() if perms != 0 { mkdirCmd += fmt.Sprintf(" -m %o", cfg.Mode) } fmt.Fprintf(b, "%s -p %q\n", mkdirCmd, dir) } if artifacts.Directories != nil { configKeys := dalec.SortMapKeys(artifacts.Directories.Config) for _, p := range configKeys { cfg := artifacts.Directories.Config[p] createArtifactDir(`%{buildroot}/%{_sysconfdir}`, p, cfg) } stateKeys := dalec.SortMapKeys(artifacts.Directories.State) for _, p := range stateKeys { cfg := artifacts.Directories.State[p] createArtifactDir(`%{buildroot}/%{_sharedstatedir}`, p, cfg) } } if len(artifacts.DataDirs) > 0 { dataFileKeys := dalec.SortMapKeys(artifacts.DataDirs) for _, k := range dataFileKeys { df := artifacts.DataDirs[k] copyArtifact(`%{buildroot}/%{_datadir}`, k, &df) } } if artifacts.Libexec != nil { libexecFileKeys := dalec.SortMapKeys(artifacts.Libexec) for _, k := range libexecFileKeys { le := artifacts.Libexec[k] copyArtifact(`%{buildroot}/%{_libexecdir}`, k, &le) } } configKeys := dalec.SortMapKeys(artifacts.ConfigFiles) for _, c := range configKeys { cfg := artifacts.ConfigFiles[c] copyArtifact(`%{buildroot}/%{_sysconfdir}`, c, &cfg) } if artifacts.Systemd != nil { serviceKeys := dalec.SortMapKeys(artifacts.Systemd.Units) for _, p := range serviceKeys { cfg := artifacts.Systemd.Units[p] // must include systemd unit extension (.service, .socket, .timer, etc.) in name copyArtifact(`%{buildroot}/%{_unitdir}`, p, cfg.Artifact()) } dropinKeys := dalec.SortMapKeys(artifacts.Systemd.Dropins) for _, d := range dropinKeys { cfg := artifacts.Systemd.Dropins[d] copyArtifact(`%{buildroot}/%{_unitdir}`, d, cfg.Artifact()) } } docKeys := dalec.SortMapKeys(artifacts.Docs) for _, d := range docKeys { cfg := artifacts.Docs[d] root := filepath.Join(`%{buildroot}/%{_docdir}`, w.Name) copyArtifact(root, d, &cfg) } licenseKeys := dalec.SortMapKeys(artifacts.Licenses) for _, l := range licenseKeys { cfg := artifacts.Licenses[l] root := filepath.Join(`%{buildroot}/%{_licensedir}`, w.Name) copyArtifact(root, l, &cfg) } libs := dalec.SortMapKeys(artifacts.Libs) for _, l := range libs { cfg := artifacts.Libs[l] root := filepath.Join(`%{buildroot}/%{_libdir}`) copyArtifact(root, l, &cfg) } for _, l := range artifacts.Links { fmt.Fprintln(b, "mkdir -p", filepath.Dir(filepath.Join("%{buildroot}", l.Dest))) fmt.Fprintln(b, "ln -sf", l.Source, "%{buildroot}/"+l.Dest) } headersKeys := dalec.SortMapKeys(artifacts.Headers) for _, h := range headersKeys { cfg := artifacts.Headers[h] copyArtifact(`%{buildroot}/%{_includedir}`, h, &cfg) } b.WriteString("\n") return b } func (w *specWrapper) Files() fmt.Stringer { b := &strings.Builder{} fmt.Fprintf(b, "%%files\n") artifacts := w.GetArtifacts(w.Target) if len(artifacts.Binaries) > 0 { binKeys := dalec.SortMapKeys(artifacts.Binaries) for _, p := range binKeys { cfg := artifacts.Binaries[p] full := filepath.Join(`%{_bindir}/`, cfg.SubPath, cfg.ResolveName(p)) fmt.Fprintln(b, full) } } if len(artifacts.Manpages) > 0 { fmt.Fprintln(b, `%{_mandir}/*/*`) } if artifacts.Directories != nil { configKeys := dalec.SortMapKeys(artifacts.Directories.Config) for _, p := range configKeys { dir := strings.Join([]string{`%dir`, filepath.Join(`%{_sysconfdir}`, p)}, " ") fmt.Fprintln(b, dir) } stateKeys := dalec.SortMapKeys(artifacts.Directories.State) for _, p := range stateKeys { dir := strings.Join([]string{`%dir`, filepath.Join(`%{_sharedstatedir}`, p)}, " ") fmt.Fprintln(b, dir) } } if artifacts.DataDirs != nil { dataKeys := dalec.SortMapKeys(artifacts.DataDirs) for _, k := range dataKeys { df := artifacts.DataDirs[k] fullPath := filepath.Join(`%{_datadir}`, df.SubPath, df.ResolveName(k)) fmt.Fprintln(b, fullPath) } } if artifacts.Libexec != nil { dataKeys := dalec.SortMapKeys(artifacts.Libexec) for _, k := range dataKeys { le := artifacts.Libexec[k] targetDir := filepath.Join(`%{_libexecdir}`, le.SubPath) fullPath := filepath.Join(targetDir, le.ResolveName(k)) fmt.Fprintln(b, fullPath) } } configKeys := dalec.SortMapKeys(artifacts.ConfigFiles) for _, c := range configKeys { cfg := artifacts.ConfigFiles[c] fullPath := filepath.Join(`%{_sysconfdir}`, cfg.SubPath, cfg.ResolveName(c)) fullDirective := strings.Join([]string{`%config(noreplace)`, fullPath}, " ") fmt.Fprintln(b, fullDirective) } if artifacts.Systemd != nil { serviceKeys := dalec.SortMapKeys(artifacts.Systemd.Units) for _, p := range serviceKeys { cfg := artifacts.Systemd.Units[p] a := cfg.Artifact() unitPath := filepath.Join(`%{_unitdir}/`, a.SubPath, a.ResolveName(p)) fmt.Fprintln(b, unitPath) } dropins := make(map[string][]string) // process these to get a unique list of files per unit name. // we need a single dir entry for the directory // need a file entry for each of files dropinKeys := dalec.SortMapKeys(artifacts.Systemd.Dropins) for _, d := range dropinKeys { cfg := artifacts.Systemd.Dropins[d] art := cfg.Artifact() files, ok := dropins[cfg.Unit] if !ok { files = []string{} } p := filepath.Join( `%{_unitdir}`, fmt.Sprintf("%s.d", cfg.Unit), art.ResolveName(d), ) dropins[cfg.Unit] = append(files, p) } unitNames := dalec.SortMapKeys(dropins) for _, u := range unitNames { dir := strings.Join([]string{ `%dir`, filepath.Join( `%{_unitdir}`, fmt.Sprintf("%s.d", u), ), }, " ") fmt.Fprintln(b, dir) for _, file := range dropins[u] { fmt.Fprintln(b, file) } } } docKeys := dalec.SortMapKeys(artifacts.Docs) for _, d := range docKeys { cfg := artifacts.Docs[d] path := filepath.Join(`%{_docdir}`, w.Name, cfg.SubPath, cfg.ResolveName(d)) fullDirective := strings.Join([]string{`%doc`, path}, " ") fmt.Fprintln(b, fullDirective) } licenseKeys := dalec.SortMapKeys(artifacts.Licenses) for _, l := range licenseKeys { cfg := artifacts.Licenses[l] path := filepath.Join(`%{_licensedir}`, w.Name, cfg.SubPath, cfg.ResolveName(l)) fullDirective := strings.Join([]string{`%license`, path}, " ") fmt.Fprintln(b, fullDirective) } libKeys := dalec.SortMapKeys(artifacts.Libs) for _, l := range libKeys { cfg := artifacts.Libs[l] path := filepath.Join(`%{_libdir}`, cfg.SubPath, cfg.ResolveName(l)) fmt.Fprintln(b, path) } for _, l := range artifacts.Links { fmt.Fprintln(b, l.Dest) } if len(artifacts.Headers) > 0 { headersKeys := dalec.SortMapKeys(artifacts.Headers) for _, h := range headersKeys { hf := artifacts.Headers[h] path := filepath.Join(`%{_includedir}`, hf.SubPath, hf.ResolveName(h)) fmt.Fprintln(b, path) } } b.WriteString("\n") return b } func (w *specWrapper) DisableStrip() string { artifacts := w.Spec.GetArtifacts(w.Target) if artifacts.DisableStrip { return "%global __strip /bin/true" } return "" } // WriteSpec generates an rpm spec from the provided [dalec.Spec] and distro target and writes it to the passed in writer func WriteSpec(spec *dalec.Spec, target string, w io.Writer) error { s := &specWrapper{spec, target} err := specTmpl.Execute(w, s) if err != nil { return fmt.Errorf("error executing spec template: %w", err) } return nil }