diff --git a/core/internal/distros/debian_obs.go b/core/internal/distros/debian_obs.go new file mode 100644 index 000000000..178f8dbc5 --- /dev/null +++ b/core/internal/distros/debian_obs.go @@ -0,0 +1,172 @@ +package distros + +import ( + "os" + "path/filepath" + "strings" +) + +const ( + obsRepoDebian13 = "Debian_13" + obsRepoDebianTesting = "Debian_Testing" + obsRepoDebianUnstable = "Debian_Unstable" +) + +func debianOBSRepoVersion(info *OSInfo, aptSuites []string) string { + hasSuite := make(map[string]bool, len(aptSuites)) + for _, suite := range aptSuites { + hasSuite[normalizeDebianSuite(suite)] = true + } + + // APT suites distinguish testing from sid. os-release cannot: both currently + // report VERSION_CODENAME=forky and PRETTY_NAME="Debian GNU/Linux forky/sid" + if hasSuite["sid"] || hasSuite["unstable"] { + return obsRepoDebianUnstable + } + if hasSuite["testing"] || hasSuite["forky"] { + return obsRepoDebianTesting + } + if hasSuite["trixie"] || hasSuite["stable"] { + return obsRepoDebian13 + } + + if info == nil { + return obsRepoDebian13 + } + + id := strings.ToLower(strings.TrimSpace(info.VersionID)) + code := strings.ToLower(strings.TrimSpace(info.VersionCodename)) + + if code == "sid" || id == "sid" { + return obsRepoDebianUnstable + } + if id == "13" || code == "trixie" { + return obsRepoDebian13 + } + if id != "" && id != "testing" { + return obsRepoDebian13 + } + // Prefer Testing when os-release is the shared forky/sid string. Unstable + // OBS packages can require a newer glibc than Testing ships. + return obsRepoDebianTesting +} + +func debianOBSListFileMatches(path, debianVersion string) bool { + data, err := os.ReadFile(path) + if err != nil { + return false + } + return debianOBSListPointsAt(string(data), debianVersion) +} + +func debianOBSListPointsAt(contents, debianVersion string) bool { + return strings.Contains(contents, "/"+debianVersion+"/") +} + +func readDebianAPTSuites() []string { + var chunks []string + if data, err := os.ReadFile("/etc/apt/sources.list"); err == nil { + chunks = append(chunks, string(data)) + } + matches, _ := filepath.Glob("/etc/apt/sources.list.d/*") + for _, path := range matches { + if !strings.HasSuffix(path, ".list") && !strings.HasSuffix(path, ".sources") { + continue + } + data, err := os.ReadFile(path) + if err != nil { + continue + } + chunks = append(chunks, string(data)) + } + return debianAPTSuites(strings.Join(chunks, "\n")) +} + +func debianAPTSuites(sources string) []string { + seen := make(map[string]bool) + var suites []string + add := func(raw string) { + suite := normalizeDebianSuite(raw) + if suite == "" || seen[suite] { + return + } + seen[suite] = true + suites = append(suites, suite) + } + + var stanza []string + flushStanza := func() { + if len(stanza) == 0 { + return + } + enabled := true + var stanzaSuites []string + for _, line := range stanza { + key, val, ok := strings.Cut(line, ":") + if !ok { + continue + } + switch strings.ToLower(strings.TrimSpace(key)) { + case "enabled": + switch strings.ToLower(strings.TrimSpace(val)) { + case "no", "false", "0": + enabled = false + } + case "suites": + stanzaSuites = strings.Fields(val) + } + } + if enabled { + for _, suite := range stanzaSuites { + add(suite) + } + } + stanza = nil + } + + for _, raw := range strings.Split(sources, "\n") { + line := strings.TrimSpace(raw) + if line == "" { + flushStanza() + continue + } + if strings.HasPrefix(line, "#") { + continue + } + lower := strings.ToLower(line) + if strings.HasPrefix(lower, "deb ") || strings.HasPrefix(lower, "deb-src ") { + flushStanza() + if suite := suiteFromDebLine(line); suite != "" { + add(suite) + } + continue + } + stanza = append(stanza, line) + } + flushStanza() + return suites +} + +func suiteFromDebLine(line string) string { + if i := strings.Index(line, "["); i != -1 { + if j := strings.Index(line, "]"); j > i { + line = strings.TrimSpace(line[:i] + " " + line[j+1:]) + } + } + fields := strings.Fields(line) + if len(fields) < 3 { + return "" + } + return fields[2] +} + +func normalizeDebianSuite(suite string) string { + suite = strings.ToLower(strings.TrimSpace(suite)) + suite = strings.Trim(suite, "/") + for _, suffix := range []string{"-security", "-updates", "-backports", "-proposed-updates"} { + if strings.HasSuffix(suite, suffix) { + return strings.TrimSuffix(suite, suffix) + } + } + return suite +}