Files
fastestmirror/internal/distro/detect.go
2025-09-13 02:47:20 +03:00

192 lines
4.6 KiB
Go

package distro
import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
)
// DistroInfo represents Linux distribution information
type DistroInfo struct {
ID string
Name string
Version string
Family string
Codename string
}
// SupportedDistros lists all distributions we can handle
var SupportedDistros = map[string]string{
"ubuntu": "debian",
"debian": "debian",
"mint": "debian",
"pop": "debian",
"elementary": "debian",
"kali": "debian",
"arch": "arch",
"manjaro": "arch",
"endeavouros": "arch",
"artix": "arch",
"fedora": "fedora",
"centos": "fedora",
"rhel": "fedora",
"rocky": "fedora",
"alma": "fedora",
"opensuse": "opensuse",
"sles": "opensuse",
"gentoo": "gentoo",
"alpine": "alpine",
"slackware": "slackware",
"void": "void",
}
// DetectDistribution attempts to detect the current Linux distribution
func DetectDistribution() (*DistroInfo, error) {
// Try /etc/os-release first (most reliable)
if info, err := parseOSRelease(); err == nil {
return info, nil
}
// Fallback to /etc/lsb-release
if info, err := parseLSBRelease(); err == nil {
return info, nil
}
// Last resort: check specific distribution files
if info, err := detectFromSpecificFiles(); err == nil {
return info, nil
}
return nil, fmt.Errorf("unable to detect Linux distribution - are you sure you're not running Windows? 🤔")
}
// parseOSRelease parses /etc/os-release file
func parseOSRelease() (*DistroInfo, error) {
return parseKeyValueFile("/etc/os-release")
}
// parseLSBRelease parses /etc/lsb-release file
func parseLSBRelease() (*DistroInfo, error) {
return parseKeyValueFile("/etc/lsb-release")
}
// parseKeyValueFile is a generic parser for key=value format files
func parseKeyValueFile(filename string) (*DistroInfo, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
info := &DistroInfo{}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.Trim(strings.TrimSpace(parts[1]), `"'`)
switch key {
case "ID", "DISTRIB_ID":
info.ID = strings.ToLower(value)
case "NAME", "DISTRIB_DESCRIPTION":
info.Name = value
case "VERSION", "DISTRIB_RELEASE":
info.Version = value
case "VERSION_CODENAME", "DISTRIB_CODENAME":
info.Codename = value
}
}
if info.ID == "" {
return nil, fmt.Errorf("could not determine distribution ID")
}
// Determine family
if family, exists := SupportedDistros[info.ID]; exists {
info.Family = family
} else {
info.Family = "unknown"
}
return info, scanner.Err()
}
// detectFromSpecificFiles tries to detect distro from specific files
func detectFromSpecificFiles() (*DistroInfo, error) {
// Map of files to expected distribution IDs
checks := map[string]string{
"/etc/arch-release": "arch",
"/etc/gentoo-release": "gentoo",
"/etc/slackware-version": "slackware",
"/etc/alpine-release": "alpine",
"/etc/void-release": "void",
}
for file, distroID := range checks {
if _, err := os.Stat(file); err == nil {
info := &DistroInfo{
ID: distroID,
Name: strings.Title(distroID),
Family: SupportedDistros[distroID],
}
// Try to get version from file content
if content, err := os.ReadFile(file); err == nil {
versionRegex := regexp.MustCompile(`\d+\.?\d*\.?\d*`)
if match := versionRegex.FindString(string(content)); match != "" {
info.Version = match
}
}
return info, nil
}
}
return nil, fmt.Errorf("no recognizable distribution files found")
}
// IsSupported checks if the distribution is supported
func (d *DistroInfo) IsSupported() bool {
_, exists := SupportedDistros[d.ID]
return exists
}
// GetConfigFile returns the path to the main package configuration file
func (d *DistroInfo) GetConfigFile() string {
switch d.Family {
case "debian":
return "/etc/apt/sources.list"
case "arch":
return "/etc/pacman.d/mirrorlist"
case "fedora":
return "/etc/yum.repos.d" // Directory for multiple files
case "opensuse":
return "/etc/zypp/repos.d" // Directory for multiple files
case "gentoo":
return "/etc/portage/make.conf"
case "alpine":
return "/etc/apk/repositories"
case "slackware":
return "/etc/slackpkg/mirrors"
default:
return ""
}
}
// String provides a human-readable representation
func (d *DistroInfo) String() string {
return fmt.Sprintf("%s %s (%s family)", d.Name, d.Version, d.Family)
}