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) }