Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion release/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ clean:
@find ../ -name '*release*.log' -delete
@find . -name '*.received.*' -delete

bin/release: $(shell find . -name "*.go")
bin/release: $(shell find . -name "*.go" -or -name "*.gotmpl")
@mkdir -p bin && \
$(call build_binary, ./cmd, bin/release)

Expand Down
208 changes: 208 additions & 0 deletions release/internal/aptrepo/apt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// Package aptrepo contains functionality for creating and managing apt repositories
package aptrepo

import (
"bytes"
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"

"github.com/sirupsen/logrus"
)

// A brief note on Ubuntu/Debian/apt repo terminology:
//
// For Ubuntu and Debian, releases are numbered and codenamed; Ubuntu releases follow
// a fixed schedule and are numbered by release year and month, e.g. 24.04 was released
// in April 2024. Debian releases do not follow a fixed schedule and are numbered
// sequentially, e.g. 12.10, 12.11, etc.
//
// The 'codename' is the one-word name of the release, such as 'noble', 'trixie',
// etc. If you run `lsb_release -a` it will give you these names in the 'Codename'
// field.
//
// Here is some terminology and how it's used for these distros and how that relates to
// use in apt repositories.
//
// Suite
// In Debian, the 'suite' refers to a category of release 'oldstable', 'stable', 'testing',
// etc., and allows users to float their version to the current 'stable' or 'testing' release
// for example; when a release is promoted to 'stable' then 'stable' refers to that new
// release (whichever it is) and users will now start to get packages from that new release;
// 'oldstable' now refers to the former 'stable'.
//
// In apt, however, this distinction is not made, and the 'suite' field can contain the codename
// of the Debian or Ubuntu release, such as 'noble', 'bookworm', etc., or the Debian 'suite'
// such as 'stable' or 'testing'.
//
// Some third party repositories will create a separate suite for their own releases; for example,
// LLVM has suites for 'llvm-toolchain-noble-18', 'llvm-toolchain-noble-19', etc. We may consider
// doing something similar, e.g. 'calico-enterprise-v3.23-noble'.
//
// Component
// Which 'part' of the release it is. Most common in Ubuntu are 'main', 'restricted',
// 'universe', and 'multiverse'; for Debian the equivalents are 'main', 'non-free-firmware',
// 'contrib', and 'non-free'.
//
// While these terms have specific meaning for these releases, we can just use 'main'
// for everything.
//
// Hopefully this explains why 'suite' and 'codename' are used mostly interchangeably in
// this code depending on what they're actually being used for!

type aptSourcesData struct {
// RepoName is the name of the repository as might be shown by repolib (e.g. in a UI)
RepoName string
// RepoURL is the base URL of the repository (i.e. where pool/ and dists/ are)
RepoURL string
// Suite is the 'suite' field, e.g. noble, bookworm, etc.
Suite string
// GpgKey is the ascii-armored GPG public key
GpgKey string
// Architectures is the list of architectures this sources file will claim support for
Architectures []string
}

//go:embed templates/repo.sources.gotmpl
var aptSourcesTemplate string

// writeAptSourcesFile creates a deb822-style sources file for a given set
// of parameters, and writes it to <suite>.sources under <rootPath>
// For more info on the format: https://repolib.readthedocs.io/en/latest/deb822-format.html
func (asd *aptSourcesData) writeAptSourcesFile(rootPath string) error {
logrus.WithField("suite", asd.Suite).Info("Generating apt .sources file")
sourcesFilePath := filepath.Join(rootPath, fmt.Sprintf("%s.sources", asd.Suite))
sourcesFile, err := os.OpenFile(sourcesFilePath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return fmt.Errorf("opening %s: %w", sourcesFilePath, err)
}
defer func() { _ = sourcesFile.Close() }()

funcMap := template.FuncMap{
"join": strings.Join,
}

tmpl, err := template.New("apt.sources").Funcs(funcMap).Parse(aptSourcesTemplate)
if err != nil {
return fmt.Errorf("failed to parse apt sources template: %w", err)
}

if err := tmpl.Execute(sourcesFile, asd); err != nil {
logrus.WithField("suite", asd.Suite).WithError(err).Error("failed to write apt sources file")
return fmt.Errorf("failed to write apt sources file: %w", err)
}

logrus.WithField("file", sourcesFilePath).Info("Wrote apt .sources file")

return nil
}

func getVersionFromDebfile(debfilePath string) (string, error) {
logrus.WithField("debfile", debfilePath).Debug("Getting version information from debian package")
cmd := exec.Command("dpkg-deb", "--show", "--showformat", "${Version}", "--", debfilePath)
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("getting version for %s: %w", debfilePath, err)
}
return string(out), nil
Comment thread
danudey marked this conversation as resolved.
}

func getComponentNameFromVersion(version string) (string, error) {
if lastIdx := strings.LastIndex(version, "~"); lastIdx != -1 {
return version[lastIdx+1:], nil
}
return "", fmt.Errorf("version %s does not contain a tilde separator", version)
}

func getSuiteNameFromDebFile(debfilePath string) (string, error) {
version, err := getVersionFromDebfile(debfilePath)
if err != nil {
return "", fmt.Errorf("getting version for %s: %w", debfilePath, err)
}

suite, err := getComponentNameFromVersion(version)
if err != nil {
return "", fmt.Errorf("getting component name for %s: %w", debfilePath, err)
}

return suite, nil
}

// formatGPGKeyForSourcesFile formats a GPG public key into a format suitable to
// be appended into a sources file template (indented one space, blank
// lines replaced with '.')
func formatGPGKeyForSourcesFile(gpgKey string) string {
// To make it easier to insert the GPG key into the sources file, we want to
// 1. Replace every blank line (there should only be one) with a '.'
// 2. Indent each line with a single space
var processedKey bytes.Buffer
lines := strings.Split(gpgKey, "\n")
// The split might result in a trailing empty string if the output ends in newline, which is typical.
// We should be careful not to add extra newlines if not present, but `gpg` output usually has a trailing newline.
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}

for _, line := range lines {
if line == "" {
line = "."
}
processedKey.WriteString(" " + line + "\n")
}

return processedKey.String()
}

func getRecursiveDebsBySuite(searchPaths []string) (map[string][]string, error) {
debsBySuite := make(map[string][]string, 0)

files, err := getRecursiveDebs(searchPaths)
if err != nil {
return map[string][]string{}, err
}

logrus.Debugf("Found %d debian package files to process", len(files))
for _, debFile := range files {
suite, err := getSuiteNameFromDebFile(debFile)
if err != nil {
return map[string][]string{}, fmt.Errorf("getting suite name for %s: %w", debFile, err)
}
debsBySuite[suite] = append(debsBySuite[suite], debFile)
}

return debsBySuite, nil
}

func getRecursiveDebs(searchPaths []string) ([]string, error) {
// Find .deb and .ddeb files
var files []string
for _, searchPath := range searchPaths {
logrus.Infof("Scanning for debian packages in %s", searchPath)
err := filepath.WalkDir(searchPath, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
// Avoid walking into .git or .aptly to save time/confusion,
// though bash script doesn't explicitly exclude them (it relies on glob).
if d.Name() == ".git" || d.Name() == "pool" {
return filepath.SkipDir
}
return nil
}
if strings.HasSuffix(path, ".deb") || strings.HasSuffix(path, ".ddeb") {
logrus.Debug(fmt.Sprintf("Found debian package %s", path))
files = append(files, path)
}
return nil
})
if err != nil {
return []string{}, fmt.Errorf("walking directory: %w", err)
}
}
return files, nil
}
Loading