netfetch/internal/collector/packages.go

526 lines
10 KiB
Go

package collector
import (
"bufio"
"database/sql"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
_ "github.com/mattn/go-sqlite3"
)
type PackageCount struct {
Manager string
Count int
}
func (c *Collector) collectPackages() {
c.mutex.Lock()
defer c.mutex.Unlock()
if !c.activeModules["packages"] {
c.info.Packages = "Unknown"
return
}
managers := detectPackageManagers()
counts := make(chan PackageCount, len(managers))
var wg sync.WaitGroup
for _, manager := range managers {
wg.Add(1)
go func(m string) {
defer wg.Done()
count := countPackages(m)
if count > 0 {
counts <- PackageCount{Manager: m, Count: count}
}
}(manager)
}
go func() {
wg.Wait()
close(counts)
}()
totalPackages := 0
var details []string
for count := range counts {
totalPackages += count.Count
details = append(details, fmt.Sprintf("%d (%s)", count.Count, count.Manager))
}
if totalPackages > 0 {
if len(details) == 1 {
c.info.Packages = details[0]
} else {
c.info.Packages = strings.Join(details, ", ")
}
} else {
c.info.Packages = "Unknown"
}
}
func detectPackageManagers() []string {
var managers []string
switch runtime.GOOS {
case "linux":
if pathExists("/var/lib/dpkg/status") {
managers = append(managers, "dpkg")
}
if pathExists("/var/lib/pacman/local") {
managers = append(managers, "pacman")
}
if pathExists("/var/lib/rpm/rpmdb.sqlite") {
managers = append(managers, "rpm-sqlite")
} else if pathExists("/var/lib/rpm/Packages") || pathExists("/var/lib/rpm/Packages.db") {
managers = append(managers, "rpm")
}
if pathExists("/var/db/pkg") && !pathExists("/var/lib/pacman") {
managers = append(managers, "emerge")
}
if pathExists("/lib/apk/db/installed") {
managers = append(managers, "apk")
}
if pathExists("/var/db/xbps") {
managers = append(managers, "xbps")
}
if isNixOS() || pathExists("/nix/var/nix/profiles") {
managers = append(managers, "nix")
}
if pathExists("/var/lib/flatpak/app") || pathExists(filepath.Join(os.Getenv("HOME"), ".local/share/flatpak/app")) {
managers = append(managers, "flatpak")
}
if pathExists("/snap") {
managers = append(managers, "snap")
}
if pathExists("/var/cache/apk") {
managers = append(managers, "apk")
}
case "darwin":
if pathExists("/usr/local/Cellar") || pathExists("/opt/homebrew/Cellar") {
managers = append(managers, "brew")
}
if pathExists("/opt/homebrew/Caskroom") || pathExists("/usr/local/Caskroom") {
managers = append(managers, "brew-cask")
}
if pathExists("/opt/local/var/macports") {
managers = append(managers, "macports")
}
if pathExists("/nix/var/nix/profiles") {
managers = append(managers, "nix")
}
case "freebsd":
if pathExists("/var/db/pkg/local.sqlite") {
managers = append(managers, "pkg-freebsd")
}
case "openbsd", "netbsd":
if pathExists("/var/db/pkg") {
managers = append(managers, "pkg-bsd")
}
}
return managers
}
func isNixOS() bool {
if data, err := os.ReadFile("/etc/os-release"); err == nil {
return strings.Contains(string(data), "ID=nixos")
}
return false
}
func countPackages(manager string) int {
switch manager {
case "dpkg":
return countDpkgPackages()
case "pacman":
return countPacmanPackages()
case "rpm-sqlite":
return countRPMSqlitePackages()
case "rpm":
return countRPMPackages()
case "emerge":
return countEmergePackages()
case "apk":
return countApkPackages()
case "xbps":
return countXbpsPackages()
case "nix":
return countNixPackages()
case "flatpak":
return countFlatpakPackages()
case "snap":
return countSnapPackages()
case "brew":
return countBrewPackages()
case "brew-cask":
return countBrewCaskPackages()
case "macports":
return countMacportsPackages()
case "pkg-freebsd":
return countPkgFreeBSDPackages()
case "pkg-bsd":
return countPkgBSDPackages()
default:
return 0
}
}
func countDpkgPackages() int {
file, err := os.Open("/var/lib/dpkg/status")
if err != nil {
return 0
}
defer file.Close()
count := 0
scanner := bufio.NewScanner(file)
var currentStatus string
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "Status:") {
currentStatus = line
} else if line == "" {
if strings.Contains(currentStatus, "install ok installed") {
count++
}
currentStatus = ""
}
}
if strings.Contains(currentStatus, "install ok installed") {
count++
}
return count
}
func countPacmanPackages() int {
entries, err := os.ReadDir("/var/lib/pacman/local")
if err != nil {
return 0
}
count := 0
for _, entry := range entries {
if entry.IsDir() && entry.Name() != "ALPM_DB_VERSION" {
count++
}
}
return count
}
func countRPMSqlitePackages() int {
db, err := sql.Open("sqlite3", "/var/lib/rpm/rpmdb.sqlite?mode=ro")
if err != nil {
return countRPMPackages()
}
defer db.Close()
var count int
err = db.QueryRow("SELECT COUNT(*) FROM Packages WHERE NOT (name LIKE 'gpg-pubkey%')").Scan(&count)
if err != nil {
return countRPMPackages()
}
return count
}
func countRPMPackages() int {
out, err := exec.Command("rpm", "-qa", "--qf", "x").Output()
if err != nil {
return 0
}
return len(string(out))
}
func countEmergePackages() int {
count := 0
baseDir := "/var/db/pkg"
categories, err := os.ReadDir(baseDir)
if err != nil {
return 0
}
for _, category := range categories {
if !category.IsDir() || strings.HasPrefix(category.Name(), ".") {
continue
}
categoryPath := filepath.Join(baseDir, category.Name())
packages, err := os.ReadDir(categoryPath)
if err != nil {
continue
}
for _, pkg := range packages {
if pkg.IsDir() && !strings.HasPrefix(pkg.Name(), ".") {
sizePath := filepath.Join(categoryPath, pkg.Name(), "SIZE")
if _, err := os.Stat(sizePath); err == nil {
count++
}
}
}
}
return count
}
func countApkPackages() int {
file, err := os.Open("/lib/apk/db/installed")
if err != nil {
return 0
}
defer file.Close()
count := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "C:Q") {
count++
}
}
return count
}
func countXbpsPackages() int {
matches, err := filepath.Glob("/var/db/xbps/pkgdb-*.plist")
if err != nil || len(matches) == 0 {
return 0
}
data, err := os.ReadFile(matches[0])
if err != nil {
return 0
}
return strings.Count(string(data), "<string>installed</string>")
}
func countNixPackages() int {
count := 0
profilePaths := []string{
"/run/current-system/sw",
filepath.Join(os.Getenv("HOME"), ".nix-profile"),
"/etc/profiles/per-user/" + os.Getenv("USER"),
"/nix/var/nix/profiles/default",
}
for _, profilePath := range profilePaths {
manifestJson := filepath.Join(profilePath, "manifest.json")
if data, err := os.ReadFile(manifestJson); err == nil {
count += strings.Count(string(data), `"name":`)
continue
}
manifestNix := filepath.Join(profilePath, "manifest.nix")
if data, err := os.ReadFile(manifestNix); err == nil {
count += strings.Count(string(data), "name = ")
continue
}
}
if count == 0 {
out, err := exec.Command("nix-store", "-qR", "/run/current-system/sw").Output()
if err == nil {
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
for _, line := range lines {
if line != "" {
count++
}
}
}
}
return count
}
func countFlatpakPackages() int {
count := 0
systemPath := "/var/lib/flatpak/app"
if entries, err := os.ReadDir(systemPath); err == nil {
for _, entry := range entries {
if entry.IsDir() {
appPath := filepath.Join(systemPath, entry.Name())
if hasCurrentSymlink(appPath) {
count++
}
}
}
}
homeDir := os.Getenv("HOME")
if homeDir != "" {
userPath := filepath.Join(homeDir, ".local/share/flatpak/app")
if entries, err := os.ReadDir(userPath); err == nil {
for _, entry := range entries {
if entry.IsDir() {
appPath := filepath.Join(userPath, entry.Name())
if hasCurrentSymlink(appPath) {
count++
}
}
}
}
}
return count
}
func hasCurrentSymlink(appPath string) bool {
arches, err := os.ReadDir(appPath)
if err != nil {
return false
}
for _, arch := range arches {
if arch.IsDir() {
branchPath := filepath.Join(appPath, arch.Name())
branches, err := os.ReadDir(branchPath)
if err != nil {
continue
}
for _, branch := range branches {
if branch.IsDir() {
currentPath := filepath.Join(branchPath, branch.Name(), "active")
if _, err := os.Lstat(currentPath); err == nil {
return true
}
}
}
}
}
return false
}
func countSnapPackages() int {
entries, err := os.ReadDir("/snap")
if err != nil {
return 0
}
count := 0
skipDirs := map[string]bool{"bin": true, "README": true}
for _, entry := range entries {
if entry.IsDir() && !skipDirs[entry.Name()] && !strings.HasPrefix(entry.Name(), ".") {
snapPath := filepath.Join("/snap", entry.Name(), "current")
if _, err := os.Lstat(snapPath); err == nil {
count++
}
}
}
return count
}
func countBrewPackages() int {
cellarPaths := []string{
"/opt/homebrew/Cellar",
"/usr/local/Cellar",
}
for _, cellarPath := range cellarPaths {
if entries, err := os.ReadDir(cellarPath); err == nil {
return len(entries)
}
}
return 0
}
func countBrewCaskPackages() int {
caskPaths := []string{
"/opt/homebrew/Caskroom",
"/usr/local/Caskroom",
}
for _, caskPath := range caskPaths {
if entries, err := os.ReadDir(caskPath); err == nil {
return len(entries)
}
}
return 0
}
func countMacportsPackages() int {
out, err := exec.Command("port", "installed").Output()
if err != nil {
return 0
}
lines := strings.Split(string(out), "\n")
count := 0
for _, line := range lines {
if strings.Contains(line, "(active)") {
count++
}
}
return count
}
func countPkgFreeBSDPackages() int {
db, err := sql.Open("sqlite3", "/var/db/pkg/local.sqlite?mode=ro")
if err != nil {
return countPkgBSDPackages()
}
defer db.Close()
var count int
err = db.QueryRow("SELECT COUNT(*) FROM packages").Scan(&count)
if err != nil {
return countPkgBSDPackages()
}
return count
}
func countPkgBSDPackages() int {
entries, err := os.ReadDir("/var/db/pkg")
if err != nil {
return 0
}
count := 0
for _, entry := range entries {
if entry.IsDir() && !strings.HasPrefix(entry.Name(), ".") {
count++
}
}
return count
}