initial commit
This commit is contained in:
783
examples/danklinux/internal/distros/arch.go
Normal file
783
examples/danklinux/internal/distros/arch.go
Normal file
@@ -0,0 +1,783 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("arch", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
Register("archarm", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
Register("archcraft", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
Register("manjaro", "#35BF5C", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
Register("obarun", "#2494be", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
Register("garuda", "#cba6f7", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type ArchDistribution struct {
|
||||
*BaseDistribution
|
||||
*ManualPackageInstaller
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewArchDistribution(config DistroConfig, logChan chan<- string) *ArchDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &ArchDistribution{
|
||||
BaseDistribution: base,
|
||||
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) GetID() string {
|
||||
return a.config.ID
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) GetColorHex() string {
|
||||
return a.config.ColorHex
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) GetFamily() DistroFamily {
|
||||
return a.config.Family
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerPacman
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return a.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
// DMS at the top (shell is prominent)
|
||||
dependencies = append(dependencies, a.detectDMS())
|
||||
|
||||
// Terminal with choice support
|
||||
dependencies = append(dependencies, a.detectSpecificTerminal(terminal))
|
||||
|
||||
// Common detections using base methods
|
||||
dependencies = append(dependencies, a.detectGit())
|
||||
dependencies = append(dependencies, a.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, a.detectQuickshell())
|
||||
dependencies = append(dependencies, a.detectXDGPortal())
|
||||
dependencies = append(dependencies, a.detectPolkitAgent())
|
||||
dependencies = append(dependencies, a.detectAccountsService())
|
||||
|
||||
// Hyprland-specific tools
|
||||
if wm == deps.WindowManagerHyprland {
|
||||
dependencies = append(dependencies, a.detectHyprlandTools()...)
|
||||
}
|
||||
|
||||
// Niri-specific tools
|
||||
if wm == deps.WindowManagerNiri {
|
||||
dependencies = append(dependencies, a.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
// Base detections (common across distros)
|
||||
dependencies = append(dependencies, a.detectMatugen())
|
||||
dependencies = append(dependencies, a.detectDgop())
|
||||
dependencies = append(dependencies, a.detectHyprpicker())
|
||||
dependencies = append(dependencies, a.detectClipboardTools()...)
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if a.packageInstalled("xdg-desktop-portal-gtk") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xdg-desktop-portal-gtk",
|
||||
Status: status,
|
||||
Description: "Desktop integration portal for GTK",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectPolkitAgent() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if a.packageInstalled("mate-polkit") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "mate-polkit",
|
||||
Status: status,
|
||||
Description: "PolicyKit authentication agent",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if a.packageInstalled("accountsservice") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "accountsservice",
|
||||
Status: status,
|
||||
Description: "D-Bus interface for user account query and manipulation",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("pacman", "-Q", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||
packages := map[string]PackageMapping{
|
||||
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
||||
"matugen": a.getMatugenMapping(variants["matugen"]),
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
packages["hyprland"] = a.getHyprlandMapping(variants["hyprland"])
|
||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
||||
packages["hyprctl"] = a.getHyprlandMapping(variants["hyprland"])
|
||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerNiri:
|
||||
packages["niri"] = a.getNiriMapping(variants["niri"])
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if forceQuickshellGit || variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "niri-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) getMatugenMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if runtime.GOARCH == "arm64" {
|
||||
return PackageMapping{Name: "matugen-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "matugen-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
return PackageMapping{Name: "matugen", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if forceDMSGit || variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
if a.packageInstalled("dms-shell-git") {
|
||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
if a.packageInstalled("dms-shell-bin") {
|
||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if a.commandExists("xwayland-satellite") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xwayland-satellite",
|
||||
Status: status,
|
||||
Description: "Xwayland support",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.06,
|
||||
Step: "Checking base-devel...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Checking if base-devel is installed",
|
||||
}
|
||||
|
||||
checkCmd := exec.CommandContext(ctx, "pacman", "-Qq", "base-devel")
|
||||
if err := checkCmd.Run(); err == nil {
|
||||
a.log("base-devel already installed")
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.10,
|
||||
Step: "base-devel already installed",
|
||||
IsComplete: false,
|
||||
LogOutput: "base-devel is already installed on the system",
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
a.log("Installing base-devel...")
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.08,
|
||||
Step: "Installing base-devel...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo pacman -S --needed --noconfirm base-devel",
|
||||
LogOutput: "Installing base-devel development tools",
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
||||
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
|
||||
return fmt.Errorf("failed to install base-devel: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.12,
|
||||
Step: "base-devel installation complete",
|
||||
IsComplete: false,
|
||||
LogOutput: "base-devel successfully installed",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error {
|
||||
// Phase 1: Check Prerequisites
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := a.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
systemPkgs, aurPkgs, manualPkgs := a.categorizePackages(dependencies, wm, reinstallFlags)
|
||||
|
||||
// Phase 3: System Packages
|
||||
if len(systemPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||
}
|
||||
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install system packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: AUR Packages
|
||||
if len(aurPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.65,
|
||||
Step: fmt.Sprintf("Installing %d AUR packages...", len(aurPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing AUR packages: %s", strings.Join(aurPkgs, ", ")),
|
||||
}
|
||||
if err := a.installAURPackages(ctx, aurPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install AUR packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: Manual Builds
|
||||
if len(manualPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.85,
|
||||
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||
}
|
||||
if err := a.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 6: Configuration
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
|
||||
// Phase 7: Complete
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool) ([]string, []string, []string) {
|
||||
systemPkgs := []string{}
|
||||
aurPkgs := []string{}
|
||||
manualPkgs := []string{}
|
||||
|
||||
variantMap := make(map[string]deps.PackageVariant)
|
||||
for _, dep := range dependencies {
|
||||
variantMap[dep.Name] = dep.Variant
|
||||
}
|
||||
|
||||
packageMap := a.GetPackageMappingWithVariants(wm, variantMap)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
// Skip installed packages unless marked for reinstall
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
// If no mapping exists, treat as manual build
|
||||
manualPkgs = append(manualPkgs, dep.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeAUR:
|
||||
aurPkgs = append(aurPkgs, pkgInfo.Name)
|
||||
case RepoTypeSystem:
|
||||
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||
case RepoTypeManual:
|
||||
manualPkgs = append(manualPkgs, dep.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return systemPkgs, aurPkgs, manualPkgs
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
||||
|
||||
hasNiri := false
|
||||
hasQuickshell := false
|
||||
for _, pkg := range packages {
|
||||
if pkg == "niri-git" {
|
||||
hasNiri = true
|
||||
}
|
||||
if pkg == "quickshell" || pkg == "quickshell-git" {
|
||||
hasQuickshell = true
|
||||
}
|
||||
}
|
||||
|
||||
// If quickshell is in the list, always reinstall google-breakpad first
|
||||
if hasQuickshell {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.63,
|
||||
Step: "Reinstalling google-breakpad for quickshell...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
|
||||
}
|
||||
|
||||
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
|
||||
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
||||
if hasNiri {
|
||||
if !a.packageInstalled("makepkg-git-lfs-proto") {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.65,
|
||||
Step: "Installing makepkg-git-lfs-proto for niri...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "Installing prerequisite for niri-git",
|
||||
}
|
||||
|
||||
if err := a.installSingleAURPackage(ctx, "makepkg-git-lfs-proto", sudoPassword, progressChan, 0.65, 0.67); err != nil {
|
||||
return fmt.Errorf("failed to install makepkg-git-lfs-proto prerequisite for niri: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder packages to ensure dms-shell-git dependencies are installed first
|
||||
orderedPackages := a.reorderAURPackages(packages)
|
||||
|
||||
baseProgress := 0.67
|
||||
progressStep := 0.13 / float64(len(orderedPackages))
|
||||
|
||||
for i, pkg := range orderedPackages {
|
||||
currentProgress := baseProgress + (float64(i) * progressStep)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: currentProgress,
|
||||
Step: fmt.Sprintf("Installing AUR package %s (%d/%d)...", pkg, i+1, len(packages)),
|
||||
IsComplete: false,
|
||||
CommandInfo: fmt.Sprintf("Building and installing %s", pkg),
|
||||
}
|
||||
|
||||
if err := a.installSingleAURPackage(ctx, pkg, sudoPassword, progressChan, currentProgress, currentProgress+progressStep); err != nil {
|
||||
return fmt.Errorf("failed to install AUR package %s: %w", pkg, err)
|
||||
}
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.80,
|
||||
Step: "All AUR packages installed successfully",
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Successfully installed AUR packages: %s", strings.Join(packages, ", ")),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
||||
dmsDepencies := []string{"quickshell", "quickshell-git", "dgop"}
|
||||
|
||||
var deps []string
|
||||
var others []string
|
||||
var dmsShell []string
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
dmsShell = append(dmsShell, pkg)
|
||||
} else {
|
||||
isDep := false
|
||||
for _, dep := range dmsDepencies {
|
||||
if pkg == dep {
|
||||
deps = append(deps, pkg)
|
||||
isDep = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isDep {
|
||||
others = append(others, pkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := append(deps, others...)
|
||||
result = append(result, dmsShell...)
|
||||
return result
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "aur-builds", pkg)
|
||||
|
||||
// Clean up any existing cache first
|
||||
if err := os.RemoveAll(buildDir); err != nil {
|
||||
a.log(fmt.Sprintf("Warning: failed to clean existing cache for %s: %v", pkg, err))
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create build directory: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if removeErr := os.RemoveAll(buildDir); removeErr != nil {
|
||||
a.log(fmt.Sprintf("Warning: failed to cleanup build directory %s: %v", buildDir, removeErr))
|
||||
}
|
||||
}()
|
||||
|
||||
// Clone the AUR package
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.1*(endProgress-startProgress),
|
||||
Step: fmt.Sprintf("Cloning %s from AUR...", pkg),
|
||||
IsComplete: false,
|
||||
CommandInfo: fmt.Sprintf("git clone https://aur.archlinux.org/%s.git", pkg),
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", fmt.Sprintf("https://aur.archlinux.org/%s.git", pkg), filepath.Join(buildDir, pkg))
|
||||
if err := a.runWithProgress(cloneCmd, progressChan, PhaseAURPackages, startProgress+0.1*(endProgress-startProgress), startProgress+0.2*(endProgress-startProgress)); err != nil {
|
||||
return fmt.Errorf("failed to clone %s: %w", pkg, err)
|
||||
}
|
||||
|
||||
packageDir := filepath.Join(buildDir, pkg)
|
||||
|
||||
if pkg == "niri-git" {
|
||||
pkgbuildPath := filepath.Join(packageDir, "PKGBUILD")
|
||||
sedCmd := exec.CommandContext(ctx, "sed", "-i", "s/makepkg-git-lfs-proto//g", pkgbuildPath)
|
||||
if err := sedCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to patch PKGBUILD for niri-git: %w", err)
|
||||
}
|
||||
|
||||
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||
sedCmd2 := exec.CommandContext(ctx, "sed", "-i", "/makedepends = makepkg-git-lfs-proto/d", srcinfoPath)
|
||||
if err := sedCmd2.Run(); err != nil {
|
||||
return fmt.Errorf("failed to patch .SRCINFO for niri-git: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||
depsToRemove := []string{
|
||||
"depends = quickshell",
|
||||
"depends = dgop",
|
||||
}
|
||||
|
||||
for _, dep := range depsToRemove {
|
||||
sedCmd := exec.CommandContext(ctx, "sed", "-i", fmt.Sprintf("/%s/d", dep), srcinfoPath)
|
||||
if err := sedCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to remove dependency %s from .SRCINFO for %s: %w", dep, pkg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all optdepends from .SRCINFO for all packages
|
||||
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||
optdepsCmd := exec.CommandContext(ctx, "sed", "-i", "/^[[:space:]]*optdepends = /d", srcinfoPath)
|
||||
if err := optdepsCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
||||
}
|
||||
|
||||
// Skip dependency installation for dms-shell-git and dms-shell-bin
|
||||
// since we manually manage those dependencies
|
||||
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
|
||||
// Pre-install dependencies from .SRCINFO
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
|
||||
IsComplete: false,
|
||||
CommandInfo: "Installing package dependencies and makedepends",
|
||||
}
|
||||
|
||||
// Install dependencies and makedepends explicitly
|
||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||
|
||||
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
fmt.Sprintf(`
|
||||
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||
if [[ "%s" == *"quickshell"* ]]; then
|
||||
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
|
||||
fi
|
||||
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
||||
fi
|
||||
`, srcinfoPath, pkg, sudoPassword))
|
||||
|
||||
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
||||
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
||||
}
|
||||
|
||||
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
fmt.Sprintf(`
|
||||
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
|
||||
if [ ! -z "$makedeps" ]; then
|
||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
|
||||
fi
|
||||
`, srcinfoPath, sudoPassword))
|
||||
|
||||
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
|
||||
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
||||
}
|
||||
} else {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
||||
}
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.4*(endProgress-startProgress),
|
||||
Step: fmt.Sprintf("Building %s...", pkg),
|
||||
IsComplete: false,
|
||||
CommandInfo: "makepkg --noconfirm",
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
||||
buildCmd.Dir = packageDir
|
||||
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
|
||||
|
||||
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
||||
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
||||
}
|
||||
|
||||
// Find built package file
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.7*(endProgress-startProgress),
|
||||
Step: fmt.Sprintf("Installing %s...", pkg),
|
||||
IsComplete: false,
|
||||
CommandInfo: "sudo pacman -U built-package",
|
||||
}
|
||||
|
||||
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
|
||||
var files []string
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
// For DMS split packages, install base package
|
||||
pattern := filepath.Join(packageDir, fmt.Sprintf("%s-%s*.pkg.tar*", pkg, "*"))
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err == nil {
|
||||
for _, match := range matches {
|
||||
basename := filepath.Base(match)
|
||||
// Always include base package
|
||||
if !strings.Contains(basename, "hyprland") && !strings.Contains(basename, "niri") {
|
||||
files = append(files, match)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also update compositor-specific packages if they're installed
|
||||
if strings.HasSuffix(pkg, "-git") {
|
||||
if a.packageInstalled("dms-shell-hyprland-git") {
|
||||
hyprlandPattern := filepath.Join(packageDir, "dms-shell-hyprland-git-*.pkg.tar*")
|
||||
if hyprlandMatches, err := filepath.Glob(hyprlandPattern); err == nil && len(hyprlandMatches) > 0 {
|
||||
files = append(files, hyprlandMatches[0])
|
||||
}
|
||||
}
|
||||
if a.packageInstalled("dms-shell-niri-git") {
|
||||
niriPattern := filepath.Join(packageDir, "dms-shell-niri-git-*.pkg.tar*")
|
||||
if niriMatches, err := filepath.Glob(niriPattern); err == nil && len(niriMatches) > 0 {
|
||||
files = append(files, niriMatches[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For other packages, install all built packages
|
||||
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
||||
files = matches
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("no package files found after building %s", pkg)
|
||||
}
|
||||
|
||||
installArgs := []string{"pacman", "-U", "--noconfirm"}
|
||||
installArgs = append(installArgs, files...)
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
||||
|
||||
fileNames := make([]string, len(files))
|
||||
for i, f := range files {
|
||||
fileNames[i] = filepath.Base(f)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.7*(endProgress-startProgress),
|
||||
LogOutput: fmt.Sprintf("Installing packages: %s", strings.Join(fileNames, ", ")),
|
||||
}
|
||||
|
||||
if err := a.runWithProgress(installCmd, progressChan, PhaseAURPackages, startProgress+0.7*(endProgress-startProgress), endProgress); err != nil {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress,
|
||||
LogOutput: fmt.Sprintf("ERROR: pacman -U failed for %s with error: %v", pkg, err),
|
||||
Error: err,
|
||||
}
|
||||
return fmt.Errorf("failed to install built package %s: %w", pkg, err)
|
||||
}
|
||||
|
||||
a.log(fmt.Sprintf("Successfully installed AUR package: %s", pkg))
|
||||
return nil
|
||||
}
|
||||
668
examples/danklinux/internal/distros/base.go
Normal file
668
examples/danklinux/internal/distros/base.go
Normal file
@@ -0,0 +1,668 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
"github.com/AvengeMedia/danklinux/internal/version"
|
||||
)
|
||||
|
||||
const forceQuickshellGit = false
|
||||
const forceDMSGit = false
|
||||
|
||||
// BaseDistribution provides common functionality for all distributions
|
||||
type BaseDistribution struct {
|
||||
logChan chan<- string
|
||||
}
|
||||
|
||||
// NewBaseDistribution creates a new base distribution
|
||||
func NewBaseDistribution(logChan chan<- string) *BaseDistribution {
|
||||
return &BaseDistribution{
|
||||
logChan: logChan,
|
||||
}
|
||||
}
|
||||
|
||||
// Common helper methods
|
||||
func (b *BaseDistribution) commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) CommandExists(cmd string) bool {
|
||||
return b.commandExists(cmd)
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) log(message string) {
|
||||
if b.logChan != nil {
|
||||
b.logChan <- message
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) logError(message string, err error) {
|
||||
errorMsg := fmt.Sprintf("ERROR: %s: %v", message, err)
|
||||
b.log(errorMsg)
|
||||
}
|
||||
|
||||
// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings.
|
||||
// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote.
|
||||
// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes.
|
||||
func escapeSingleQuotes(s string) string {
|
||||
return strings.ReplaceAll(s, "'", "'\\''")
|
||||
}
|
||||
|
||||
// MakeSudoCommand creates a command string that safely passes password to sudo.
|
||||
// This helper escapes special characters in the password to prevent shell injection
|
||||
// and syntax errors when passwords contain single quotes, apostrophes, or other special chars.
|
||||
func MakeSudoCommand(sudoPassword string, command string) string {
|
||||
return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command)
|
||||
}
|
||||
|
||||
// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password.
|
||||
// The password is properly escaped to prevent shell injection and syntax errors.
|
||||
func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
|
||||
cmdStr := MakeSudoCommand(sudoPassword, command)
|
||||
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
||||
}
|
||||
|
||||
// Keep unexported versions for backward compatibility within package
|
||||
func makeSudoCommand(sudoPassword string, command string) string {
|
||||
return MakeSudoCommand(sudoPassword, command)
|
||||
}
|
||||
|
||||
func execSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
|
||||
return ExecSudoCommand(ctx, sudoPassword, command)
|
||||
}
|
||||
|
||||
// Common dependency detection methods
|
||||
func (b *BaseDistribution) detectGit() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("git") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "git",
|
||||
Status: status,
|
||||
Description: "Version control system",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectMatugen() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("matugen") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "matugen",
|
||||
Status: status,
|
||||
Description: "Material Design color generation tool",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectDgop() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("dgop") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "dgop",
|
||||
Status: status,
|
||||
Description: "Desktop portal management tool",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectDMS() deps.Dependency {
|
||||
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
|
||||
|
||||
status := deps.StatusMissing
|
||||
currentVersion := ""
|
||||
|
||||
if _, err := os.Stat(dmsPath); err == nil {
|
||||
status = deps.StatusInstalled
|
||||
|
||||
// Only get current version, don't check for updates (lazy loading)
|
||||
current, err := version.GetCurrentDMSVersion()
|
||||
if err == nil {
|
||||
currentVersion = current
|
||||
}
|
||||
}
|
||||
|
||||
dep := deps.Dependency{
|
||||
Name: "dms (DankMaterialShell)",
|
||||
Status: status,
|
||||
Description: "Desktop Management System configuration",
|
||||
Required: true,
|
||||
CanToggle: true,
|
||||
}
|
||||
|
||||
if currentVersion != "" {
|
||||
dep.Version = currentVersion
|
||||
}
|
||||
|
||||
return dep
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectSpecificTerminal(terminal deps.Terminal) deps.Dependency {
|
||||
switch terminal {
|
||||
case deps.TerminalGhostty:
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("ghostty") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "ghostty",
|
||||
Status: status,
|
||||
Description: "A fast, native terminal emulator built in Zig.",
|
||||
Required: true,
|
||||
}
|
||||
case deps.TerminalKitty:
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("kitty") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "kitty",
|
||||
Status: status,
|
||||
Description: "A feature-rich, customizable terminal emulator.",
|
||||
Required: true,
|
||||
}
|
||||
case deps.TerminalAlacritty:
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("alacritty") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "alacritty",
|
||||
Status: status,
|
||||
Description: "A simple terminal emulator. (No dynamic theming)",
|
||||
Required: true,
|
||||
}
|
||||
default:
|
||||
return b.detectSpecificTerminal(deps.TerminalGhostty)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
cliphist := deps.StatusMissing
|
||||
if b.commandExists("cliphist") {
|
||||
cliphist = deps.StatusInstalled
|
||||
}
|
||||
|
||||
wlClipboard := deps.StatusMissing
|
||||
if b.commandExists("wl-copy") && b.commandExists("wl-paste") {
|
||||
wlClipboard = deps.StatusInstalled
|
||||
}
|
||||
|
||||
dependencies = append(dependencies,
|
||||
deps.Dependency{
|
||||
Name: "cliphist",
|
||||
Status: cliphist,
|
||||
Description: "Wayland clipboard manager",
|
||||
Required: true,
|
||||
},
|
||||
deps.Dependency{
|
||||
Name: "wl-clipboard",
|
||||
Status: wlClipboard,
|
||||
Description: "Wayland clipboard utilities",
|
||||
Required: true,
|
||||
},
|
||||
)
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectHyprpicker() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("hyprpicker") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "hyprpicker",
|
||||
Status: status,
|
||||
Description: "Color picker for Wayland",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
tools := []struct {
|
||||
name string
|
||||
description string
|
||||
}{
|
||||
{"grim", "Screenshot utility for Wayland"},
|
||||
{"slurp", "Region selection utility for Wayland"},
|
||||
{"hyprctl", "Hyprland control utility"},
|
||||
{"grimblast", "Screenshot script for Hyprland"},
|
||||
{"jq", "JSON processor"},
|
||||
}
|
||||
|
||||
for _, tool := range tools {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists(tool.name) {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
dependencies = append(dependencies, deps.Dependency{
|
||||
Name: tool.name,
|
||||
Status: status,
|
||||
Description: tool.description,
|
||||
Required: true,
|
||||
})
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectQuickshell() deps.Dependency {
|
||||
if !b.commandExists("qs") {
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusMissing,
|
||||
Description: "QtQuick based desktop shell toolkit",
|
||||
Required: true,
|
||||
Variant: deps.VariantStable,
|
||||
CanToggle: true,
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("qs", "--version")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusNeedsReinstall,
|
||||
Description: "QtQuick based desktop shell toolkit (version check failed)",
|
||||
Required: true,
|
||||
Variant: deps.VariantStable,
|
||||
CanToggle: true,
|
||||
}
|
||||
}
|
||||
|
||||
versionStr := string(output)
|
||||
versionRegex := regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
||||
matches := versionRegex.FindStringSubmatch(versionStr)
|
||||
|
||||
if len(matches) < 2 {
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusNeedsReinstall,
|
||||
Description: "QtQuick based desktop shell toolkit (unknown version)",
|
||||
Required: true,
|
||||
Variant: deps.VariantStable,
|
||||
CanToggle: true,
|
||||
}
|
||||
}
|
||||
|
||||
version := matches[1]
|
||||
variant := deps.VariantStable
|
||||
if strings.Contains(versionStr, "git") || strings.Contains(versionStr, "+") {
|
||||
variant = deps.VariantGit
|
||||
}
|
||||
|
||||
if b.versionCompare(version, "0.2.0") >= 0 {
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusInstalled,
|
||||
Version: version,
|
||||
Description: "QtQuick based desktop shell toolkit",
|
||||
Required: true,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
}
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusNeedsUpdate,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
Version: version,
|
||||
Description: "QtQuick based desktop shell toolkit (needs 0.2.0+)",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectWindowManager(wm deps.WindowManager) deps.Dependency {
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
status := deps.StatusMissing
|
||||
variant := deps.VariantStable
|
||||
version := ""
|
||||
|
||||
if b.commandExists("hyprland") || b.commandExists("Hyprland") {
|
||||
status = deps.StatusInstalled
|
||||
cmd := exec.Command("hyprctl", "version")
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
outStr := string(output)
|
||||
if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") {
|
||||
variant = deps.VariantGit
|
||||
}
|
||||
if versionRegex := regexp.MustCompile(`v(\d+\.\d+\.\d+)`); versionRegex.MatchString(outStr) {
|
||||
matches := versionRegex.FindStringSubmatch(outStr)
|
||||
if len(matches) > 1 {
|
||||
version = matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "hyprland",
|
||||
Status: status,
|
||||
Version: version,
|
||||
Description: "Dynamic tiling Wayland compositor",
|
||||
Required: true,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
}
|
||||
case deps.WindowManagerNiri:
|
||||
status := deps.StatusMissing
|
||||
variant := deps.VariantStable
|
||||
version := ""
|
||||
|
||||
if b.commandExists("niri") {
|
||||
status = deps.StatusInstalled
|
||||
cmd := exec.Command("niri", "--version")
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
outStr := string(output)
|
||||
if strings.Contains(outStr, "git") || strings.Contains(outStr, "+") {
|
||||
variant = deps.VariantGit
|
||||
}
|
||||
if versionRegex := regexp.MustCompile(`niri (\d+\.\d+)`); versionRegex.MatchString(outStr) {
|
||||
matches := versionRegex.FindStringSubmatch(outStr)
|
||||
if len(matches) > 1 {
|
||||
version = matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "niri",
|
||||
Status: status,
|
||||
Version: version,
|
||||
Description: "Scrollable-tiling Wayland compositor",
|
||||
Required: true,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
}
|
||||
default:
|
||||
return deps.Dependency{
|
||||
Name: "unknown-wm",
|
||||
Status: deps.StatusMissing,
|
||||
Description: "Unknown window manager",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Version comparison helper
|
||||
func (b *BaseDistribution) versionCompare(v1, v2 string) int {
|
||||
parts1 := strings.Split(v1, ".")
|
||||
parts2 := strings.Split(v2, ".")
|
||||
|
||||
for i := 0; i < len(parts1) && i < len(parts2); i++ {
|
||||
if parts1[i] < parts2[i] {
|
||||
return -1
|
||||
}
|
||||
if parts1[i] > parts2[i] {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts1) < len(parts2) {
|
||||
return -1
|
||||
}
|
||||
if len(parts1) > len(parts2) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Common installation helper
|
||||
func (b *BaseDistribution) runWithProgress(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64) error {
|
||||
return b.runWithProgressTimeout(cmd, progressChan, phase, startProgress, endProgress, 20*time.Minute)
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) runWithProgressTimeout(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, timeout time.Duration) error {
|
||||
return b.runWithProgressStepTimeout(cmd, progressChan, phase, startProgress, endProgress, "Installing...", timeout)
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) runWithProgressStep(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, stepMessage string) error {
|
||||
return b.runWithProgressStepTimeout(cmd, progressChan, phase, startProgress, endProgress, stepMessage, 20*time.Minute)
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) runWithProgressStepTimeout(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, stepMessage string, timeoutDuration time.Duration) error {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stdout pipe: %w", err)
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stderr pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputChan := make(chan string, 100)
|
||||
done := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
b.log(line)
|
||||
outputChan <- line
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
b.log(line)
|
||||
outputChan <- line
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
close(outputChan)
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(200 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
progress := startProgress
|
||||
progressStep := (endProgress - startProgress) / 50
|
||||
lastOutput := ""
|
||||
|
||||
var timeout *time.Timer
|
||||
var timeoutChan <-chan time.Time
|
||||
if timeoutDuration > 0 {
|
||||
timeout = time.NewTimer(timeoutDuration)
|
||||
defer timeout.Stop()
|
||||
timeoutChan = timeout.C
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
b.logError("Command execution failed", err)
|
||||
b.log(fmt.Sprintf("Last output before failure: %s", lastOutput))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: startProgress,
|
||||
Step: "Command failed",
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
Error: err,
|
||||
}
|
||||
return err
|
||||
}
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: endProgress,
|
||||
Step: "Installation step complete",
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
}
|
||||
return nil
|
||||
case output, ok := <-outputChan:
|
||||
if ok {
|
||||
lastOutput = output
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: progress,
|
||||
Step: stepMessage,
|
||||
IsComplete: false,
|
||||
LogOutput: output,
|
||||
}
|
||||
if timeout != nil {
|
||||
timeout.Reset(timeoutDuration)
|
||||
}
|
||||
}
|
||||
case <-timeoutChan:
|
||||
if cmd.Process != nil {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
err := fmt.Errorf("installation timed out after %v", timeoutDuration)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: startProgress,
|
||||
Step: "Installation timed out",
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
Error: err,
|
||||
}
|
||||
return err
|
||||
case <-ticker.C:
|
||||
if progress < endProgress-0.01 {
|
||||
progress += progressStep
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: progress,
|
||||
Step: "Installing...",
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// installDMSBinary installs the DMS binary from GitHub releases
|
||||
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
b.log("Installing/updating DMS binary...")
|
||||
|
||||
// Detect architecture
|
||||
arch := runtime.GOARCH
|
||||
switch arch {
|
||||
case "amd64":
|
||||
case "arm64":
|
||||
default:
|
||||
return fmt.Errorf("unsupported architecture for DMS: %s", arch)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.80,
|
||||
Step: "Downloading DMS binary...",
|
||||
IsComplete: false,
|
||||
CommandInfo: fmt.Sprintf("Downloading dms-%s.gz", arch),
|
||||
}
|
||||
|
||||
// Get latest release version
|
||||
latestVersionCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
`curl -s https://api.github.com/repos/AvengeMedia/danklinux/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'`)
|
||||
versionOutput, err := latestVersionCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get latest DMS version: %w", err)
|
||||
}
|
||||
version := strings.TrimSpace(string(versionOutput))
|
||||
if version == "" {
|
||||
return fmt.Errorf("could not determine latest DMS version")
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "manual-builds")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Download the gzipped binary
|
||||
downloadURL := fmt.Sprintf("https://github.com/AvengeMedia/danklinux/releases/download/%s/dms-%s.gz", version, arch)
|
||||
gzPath := filepath.Join(tmpDir, "dms.gz")
|
||||
|
||||
downloadCmd := exec.CommandContext(ctx, "curl", "-L", downloadURL, "-o", gzPath)
|
||||
if err := downloadCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to download DMS binary: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.85,
|
||||
Step: "Extracting DMS binary...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "gunzip dms.gz",
|
||||
}
|
||||
|
||||
// Extract the binary
|
||||
extractCmd := exec.CommandContext(ctx, "gunzip", gzPath)
|
||||
if err := extractCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to extract DMS binary: %w", err)
|
||||
}
|
||||
|
||||
binaryPath := filepath.Join(tmpDir, "dms")
|
||||
|
||||
// Make it executable
|
||||
chmodCmd := exec.CommandContext(ctx, "chmod", "+x", binaryPath)
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to make DMS binary executable: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.88,
|
||||
Step: "Installing DMS to /usr/local/bin...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo cp dms /usr/local/bin/",
|
||||
}
|
||||
|
||||
// Install to /usr/local/bin
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install DMS binary: %w", err)
|
||||
}
|
||||
|
||||
b.log("DMS binary installed successfully")
|
||||
return nil
|
||||
}
|
||||
220
examples/danklinux/internal/distros/base_test.go
Normal file
220
examples/danklinux/internal/distros/base_test.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
|
||||
originalHome := os.Getenv("HOME")
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
os.Setenv("HOME", tempDir)
|
||||
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
dep := base.detectDMS()
|
||||
|
||||
if dep.Status != deps.StatusMissing {
|
||||
t.Errorf("Expected StatusMissing, got %d", dep.Status)
|
||||
}
|
||||
|
||||
if dep.Name != "dms (DankMaterialShell)" {
|
||||
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
|
||||
}
|
||||
|
||||
if !dep.Required {
|
||||
t.Error("Expected Required to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
||||
if !commandExists("git") {
|
||||
t.Skip("git not available")
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||
os.MkdirAll(dmsPath, 0755)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
os.Setenv("HOME", tempDir)
|
||||
|
||||
exec.Command("git", "init", dmsPath).Run()
|
||||
exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run()
|
||||
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
|
||||
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
|
||||
|
||||
testFile := filepath.Join(dmsPath, "test.txt")
|
||||
os.WriteFile(testFile, []byte("test"), 0644)
|
||||
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
dep := base.detectDMS()
|
||||
|
||||
if dep.Status == deps.StatusMissing {
|
||||
t.Error("Expected DMS to be detected as installed")
|
||||
}
|
||||
|
||||
if dep.Name != "dms (DankMaterialShell)" {
|
||||
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
|
||||
}
|
||||
|
||||
if !dep.Required {
|
||||
t.Error("Expected Required to be true")
|
||||
}
|
||||
|
||||
t.Logf("Status: %d, Version: %s", dep.Status, dep.Version)
|
||||
}
|
||||
|
||||
func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
||||
if !commandExists("git") {
|
||||
t.Skip("git not available")
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||
os.MkdirAll(dmsPath, 0755)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
os.Setenv("HOME", tempDir)
|
||||
|
||||
exec.Command("git", "init", dmsPath).Run()
|
||||
exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run()
|
||||
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
|
||||
exec.Command("git", "-C", dmsPath, "remote", "add", "origin", "https://github.com/AvengeMedia/DankMaterialShell.git").Run()
|
||||
|
||||
testFile := filepath.Join(dmsPath, "test.txt")
|
||||
os.WriteFile(testFile, []byte("test"), 0644)
|
||||
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||
exec.Command("git", "-C", dmsPath, "tag", "v0.0.1").Run()
|
||||
exec.Command("git", "-C", dmsPath, "checkout", "v0.0.1").Run()
|
||||
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
dep := base.detectDMS()
|
||||
|
||||
if dep.Name != "dms (DankMaterialShell)" {
|
||||
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
|
||||
}
|
||||
|
||||
if !dep.Required {
|
||||
t.Error("Expected Required to be true")
|
||||
}
|
||||
|
||||
t.Logf("Status: %d, Version: %s", dep.Status, dep.Version)
|
||||
}
|
||||
|
||||
func TestBaseDistribution_detectDMS_DirectoryWithoutGit(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||
os.MkdirAll(dmsPath, 0755)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
os.Setenv("HOME", tempDir)
|
||||
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
dep := base.detectDMS()
|
||||
|
||||
if dep.Status == deps.StatusMissing {
|
||||
t.Error("Expected DMS to be detected as present")
|
||||
}
|
||||
|
||||
if dep.Name != "dms (DankMaterialShell)" {
|
||||
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
|
||||
}
|
||||
|
||||
if !dep.Required {
|
||||
t.Error("Expected Required to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseDistribution_NewBaseDistribution(t *testing.T) {
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
|
||||
if base == nil {
|
||||
t.Fatal("NewBaseDistribution returned nil")
|
||||
}
|
||||
|
||||
if base.logChan == nil {
|
||||
t.Error("logChan was not set")
|
||||
}
|
||||
}
|
||||
|
||||
func commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func TestBaseDistribution_versionCompare(t *testing.T) {
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
|
||||
tests := []struct {
|
||||
v1 string
|
||||
v2 string
|
||||
expected int
|
||||
}{
|
||||
{"0.1.0", "0.1.0", 0},
|
||||
{"0.1.0", "0.1.1", -1},
|
||||
{"0.1.1", "0.1.0", 1},
|
||||
{"0.2.0", "0.1.9", 1},
|
||||
{"1.0.0", "0.9.9", 1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := base.versionCompare(tt.v1, tt.v2)
|
||||
if result != tt.expected {
|
||||
t.Errorf("versionCompare(%q, %q) = %d; want %d", tt.v1, tt.v2, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseDistribution_versionCompare_WithPrefix(t *testing.T) {
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
|
||||
tests := []struct {
|
||||
v1 string
|
||||
v2 string
|
||||
expected int
|
||||
}{
|
||||
{"v0.1.0", "v0.1.0", 0},
|
||||
{"v0.1.0", "v0.1.1", -1},
|
||||
{"v0.1.1", "v0.1.0", 1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := base.versionCompare(tt.v1, tt.v2)
|
||||
if result != tt.expected {
|
||||
t.Errorf("versionCompare(%q, %q) = %d; want %d", tt.v1, tt.v2, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
533
examples/danklinux/internal/distros/debian.go
Normal file
533
examples/danklinux/internal/distros/debian.go
Normal file
@@ -0,0 +1,533 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("debian", "#A80030", FamilyDebian, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewDebianDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type DebianDistribution struct {
|
||||
*BaseDistribution
|
||||
*ManualPackageInstaller
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewDebianDistribution(config DistroConfig, logChan chan<- string) *DebianDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &DebianDistribution{
|
||||
BaseDistribution: base,
|
||||
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) GetID() string {
|
||||
return d.config.ID
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) GetColorHex() string {
|
||||
return d.config.ColorHex
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) GetFamily() DistroFamily {
|
||||
return d.config.Family
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerAPT
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return d.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
dependencies = append(dependencies, d.detectDMS())
|
||||
|
||||
dependencies = append(dependencies, d.detectSpecificTerminal(terminal))
|
||||
|
||||
dependencies = append(dependencies, d.detectGit())
|
||||
dependencies = append(dependencies, d.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, d.detectQuickshell())
|
||||
dependencies = append(dependencies, d.detectXDGPortal())
|
||||
dependencies = append(dependencies, d.detectPolkitAgent())
|
||||
dependencies = append(dependencies, d.detectAccountsService())
|
||||
|
||||
if wm == deps.WindowManagerNiri {
|
||||
dependencies = append(dependencies, d.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
dependencies = append(dependencies, d.detectMatugen())
|
||||
dependencies = append(dependencies, d.detectDgop())
|
||||
dependencies = append(dependencies, d.detectHyprpicker())
|
||||
dependencies = append(dependencies, d.detectClipboardTools()...)
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if d.packageInstalled("xdg-desktop-portal-gtk") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xdg-desktop-portal-gtk",
|
||||
Status: status,
|
||||
Description: "Desktop integration portal for GTK",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) detectPolkitAgent() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if d.packageInstalled("mate-polkit") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "mate-polkit",
|
||||
Status: status,
|
||||
Description: "PolicyKit authentication agent",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if d.commandExists("xwayland-satellite") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xwayland-satellite",
|
||||
Status: status,
|
||||
Description: "Xwayland support",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) detectAccountsService() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if d.packageInstalled("accountsservice") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "accountsservice",
|
||||
Status: status,
|
||||
Description: "D-Bus interface for user account query and manipulation",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("dpkg", "-l", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
packages := map[string]PackageMapping{
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||
|
||||
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
||||
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
|
||||
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
|
||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeManual, BuildFunc: "installHyprpicker"},
|
||||
}
|
||||
|
||||
if wm == deps.WindowManagerNiri {
|
||||
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.06,
|
||||
Step: "Updating package lists...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Updating APT package lists",
|
||||
}
|
||||
|
||||
updateCmd := execSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||
return fmt.Errorf("failed to update package lists: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.08,
|
||||
Step: "Installing build-essential...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install -y build-essential",
|
||||
LogOutput: "Installing build tools",
|
||||
}
|
||||
|
||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
cmd := execSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
||||
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.10,
|
||||
Step: "Installing development dependencies...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
||||
LogOutput: "Installing additional development tools",
|
||||
}
|
||||
|
||||
devToolsCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev")
|
||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||
return fmt.Errorf("failed to install development tools: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.12,
|
||||
Step: "Prerequisites installation complete",
|
||||
IsComplete: false,
|
||||
LogOutput: "Prerequisites successfully installed",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := d.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
systemPkgs, manualPkgs := d.categorizePackages(dependencies, wm, reinstallFlags)
|
||||
|
||||
if len(systemPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||
}
|
||||
if err := d.installAPTPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install APT packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(manualPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.80,
|
||||
Step: "Installing build dependencies...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Installing build tools for manual compilation",
|
||||
}
|
||||
if err := d.installBuildDependencies(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install build dependencies: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.85,
|
||||
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||
}
|
||||
if err := d.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool) ([]string, []string) {
|
||||
systemPkgs := []string{}
|
||||
manualPkgs := []string{}
|
||||
|
||||
packageMap := d.GetPackageMapping(wm)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
d.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeSystem:
|
||||
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||
case RepoTypeManual:
|
||||
manualPkgs = append(manualPkgs, dep.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return systemPkgs, manualPkgs
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
buildDeps := make(map[string]bool)
|
||||
|
||||
for _, pkg := range manualPkgs {
|
||||
switch pkg {
|
||||
case "niri":
|
||||
buildDeps["curl"] = true
|
||||
buildDeps["libxkbcommon-dev"] = true
|
||||
buildDeps["libwayland-dev"] = true
|
||||
buildDeps["libudev-dev"] = true
|
||||
buildDeps["libinput-dev"] = true
|
||||
buildDeps["libdisplay-info-dev"] = true
|
||||
buildDeps["libpango1.0-dev"] = true
|
||||
buildDeps["libcairo-dev"] = true
|
||||
buildDeps["libpipewire-0.3-dev"] = true
|
||||
buildDeps["libc6-dev"] = true
|
||||
buildDeps["clang"] = true
|
||||
buildDeps["libseat-dev"] = true
|
||||
buildDeps["libgbm-dev"] = true
|
||||
buildDeps["alacritty"] = true
|
||||
buildDeps["fuzzel"] = true
|
||||
case "quickshell":
|
||||
buildDeps["qt6-base-dev"] = true
|
||||
buildDeps["qt6-base-private-dev"] = true
|
||||
buildDeps["qt6-declarative-dev"] = true
|
||||
buildDeps["qt6-declarative-private-dev"] = true
|
||||
buildDeps["qt6-wayland-dev"] = true
|
||||
buildDeps["qt6-wayland-private-dev"] = true
|
||||
buildDeps["qt6-tools-dev"] = true
|
||||
buildDeps["libqt6svg6-dev"] = true
|
||||
buildDeps["qt6-shadertools-dev"] = true
|
||||
buildDeps["spirv-tools"] = true
|
||||
buildDeps["libcli11-dev"] = true
|
||||
buildDeps["libjemalloc-dev"] = true
|
||||
buildDeps["libwayland-dev"] = true
|
||||
buildDeps["wayland-protocols"] = true
|
||||
buildDeps["libdrm-dev"] = true
|
||||
buildDeps["libgbm-dev"] = true
|
||||
buildDeps["libegl-dev"] = true
|
||||
buildDeps["libgles2-mesa-dev"] = true
|
||||
buildDeps["libgl1-mesa-dev"] = true
|
||||
buildDeps["libxcb1-dev"] = true
|
||||
buildDeps["libpipewire-0.3-dev"] = true
|
||||
buildDeps["libpam0g-dev"] = true
|
||||
case "ghostty":
|
||||
buildDeps["curl"] = true
|
||||
case "matugen":
|
||||
buildDeps["curl"] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, pkg := range manualPkgs {
|
||||
switch pkg {
|
||||
case "niri", "matugen":
|
||||
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Rust: %w", err)
|
||||
}
|
||||
case "cliphist", "dgop":
|
||||
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Go: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(buildDeps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
depList := make([]string, 0, len(buildDeps))
|
||||
for dep := range buildDeps {
|
||||
depList = append(depList, dep)
|
||||
}
|
||||
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, depList...)
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if d.commandExists("cargo") {
|
||||
return nil
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.82,
|
||||
Step: "Installing rustup...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install rustup",
|
||||
}
|
||||
|
||||
rustupInstallCmd := execSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
||||
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||
return fmt.Errorf("failed to install rustup: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.83,
|
||||
Step: "Installing stable Rust toolchain...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "rustup install stable",
|
||||
}
|
||||
|
||||
rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable")
|
||||
if err := d.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil {
|
||||
return fmt.Errorf("failed to install Rust toolchain: %w", err)
|
||||
}
|
||||
|
||||
if !d.commandExists("cargo") {
|
||||
d.log("Warning: cargo not found in PATH after Rust installation, trying to source environment")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if d.commandExists("go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.87,
|
||||
Step: "Installing Go...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install golang-go",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
||||
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) installGhosttyDebian(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
d.log("Installing Ghostty using Debian installer script...")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Running Ghostty Debian installer...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
|
||||
LogOutput: "Installing Ghostty using pre-built Debian package",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
|
||||
|
||||
if err := d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
|
||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||
}
|
||||
|
||||
d.log("Ghostty installed successfully using Debian installer")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
for _, pkg := range packages {
|
||||
switch pkg {
|
||||
case "ghostty":
|
||||
if err := d.installGhosttyDebian(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||
}
|
||||
default:
|
||||
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
19
examples/danklinux/internal/distros/factory.go
Normal file
19
examples/danklinux/internal/distros/factory.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
// NewDependencyDetector creates a DependencyDetector for the specified distribution
|
||||
func NewDependencyDetector(distribution string, logChan chan<- string) (deps.DependencyDetector, error) {
|
||||
distro, err := NewDistribution(distribution, logChan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return distro, nil
|
||||
}
|
||||
|
||||
// NewPackageInstaller creates a Distribution for package installation
|
||||
func NewPackageInstaller(distribution string, logChan chan<- string) (Distribution, error) {
|
||||
return NewDistribution(distribution, logChan)
|
||||
}
|
||||
550
examples/danklinux/internal/distros/fedora.go
Normal file
550
examples/danklinux/internal/distros/fedora.go
Normal file
@@ -0,0 +1,550 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
|
||||
Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type FedoraDistribution struct {
|
||||
*BaseDistribution
|
||||
*ManualPackageInstaller
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewFedoraDistribution(config DistroConfig, logChan chan<- string) *FedoraDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &FedoraDistribution{
|
||||
BaseDistribution: base,
|
||||
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) GetID() string {
|
||||
return f.config.ID
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) GetColorHex() string {
|
||||
return f.config.ColorHex
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) GetFamily() DistroFamily {
|
||||
return f.config.Family
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerDNF
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return f.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
// DMS at the top (shell is prominent)
|
||||
dependencies = append(dependencies, f.detectDMS())
|
||||
|
||||
// Terminal with choice support
|
||||
dependencies = append(dependencies, f.detectSpecificTerminal(terminal))
|
||||
|
||||
// Common detections using base methods
|
||||
dependencies = append(dependencies, f.detectGit())
|
||||
dependencies = append(dependencies, f.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, f.detectQuickshell())
|
||||
dependencies = append(dependencies, f.detectXDGPortal())
|
||||
dependencies = append(dependencies, f.detectPolkitAgent())
|
||||
dependencies = append(dependencies, f.detectAccountsService())
|
||||
|
||||
// Hyprland-specific tools
|
||||
if wm == deps.WindowManagerHyprland {
|
||||
dependencies = append(dependencies, f.detectHyprlandTools()...)
|
||||
}
|
||||
|
||||
// Niri-specific tools
|
||||
if wm == deps.WindowManagerNiri {
|
||||
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
// Base detections (common across distros)
|
||||
dependencies = append(dependencies, f.detectMatugen())
|
||||
dependencies = append(dependencies, f.detectDgop())
|
||||
dependencies = append(dependencies, f.detectHyprpicker())
|
||||
dependencies = append(dependencies, f.detectClipboardTools()...)
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if f.packageInstalled("xdg-desktop-portal-gtk") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xdg-desktop-portal-gtk",
|
||||
Status: status,
|
||||
Description: "Desktop integration portal for GTK",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) detectPolkitAgent() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if f.packageInstalled("mate-polkit") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "mate-polkit",
|
||||
Status: status,
|
||||
Description: "PolicyKit authentication agent",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("rpm", "-q", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
return f.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||
packages := map[string]PackageMapping{
|
||||
// Standard DNF packages
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||
"hyprpicker": f.getHyprpickerMapping(variants["hyprland"]),
|
||||
|
||||
// COPR packages
|
||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"])
|
||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
||||
packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"])
|
||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerNiri:
|
||||
packages["niri"] = f.getNiriMapping(variants["niri"])
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if forceQuickshellGit || variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
||||
}
|
||||
return PackageMapping{Name: "quickshell", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms-git"}
|
||||
}
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
||||
}
|
||||
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getHyprpickerMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "hyprpicker-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
||||
}
|
||||
return PackageMapping{Name: "hyprpicker", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
|
||||
}
|
||||
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if f.commandExists("xwayland-satellite") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xwayland-satellite",
|
||||
Status: status,
|
||||
Description: "Xwayland support",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) detectAccountsService() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if f.packageInstalled("accountsservice") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "accountsservice",
|
||||
Status: status,
|
||||
Description: "D-Bus interface for user account query and manipulation",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getPrerequisites() []string {
|
||||
return []string{
|
||||
"dnf-plugins-core",
|
||||
"make",
|
||||
"unzip",
|
||||
"libwayland-server",
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
prerequisites := f.getPrerequisites()
|
||||
var missingPkgs []string
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.06,
|
||||
Step: "Checking prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Checking prerequisite packages",
|
||||
}
|
||||
|
||||
for _, pkg := range prerequisites {
|
||||
checkCmd := exec.CommandContext(ctx, "rpm", "-q", pkg)
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
missingPkgs = append(missingPkgs, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
f.log("go not found in PATH, will install golang-bin")
|
||||
missingPkgs = append(missingPkgs, "golang-bin")
|
||||
} else {
|
||||
f.log("go already available in PATH")
|
||||
}
|
||||
|
||||
if len(missingPkgs) == 0 {
|
||||
f.log("All prerequisites already installed")
|
||||
return nil
|
||||
}
|
||||
|
||||
f.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.08,
|
||||
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo dnf install -y %s", strings.Join(missingPkgs, " ")),
|
||||
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
|
||||
}
|
||||
|
||||
args := []string{"dnf", "install", "-y"}
|
||||
args = append(args, missingPkgs...)
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logError("failed to install prerequisites", err)
|
||||
f.log(fmt.Sprintf("Prerequisites command output: %s", string(output)))
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
f.log(fmt.Sprintf("Prerequisites install output: %s", string(output)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error {
|
||||
// Phase 1: Check Prerequisites
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := f.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
dnfPkgs, coprPkgs, manualPkgs := f.categorizePackages(dependencies, wm, reinstallFlags)
|
||||
|
||||
// Phase 2: Enable COPR repositories
|
||||
if len(coprPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.15,
|
||||
Step: "Enabling COPR repositories...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Setting up COPR repositories for additional packages",
|
||||
}
|
||||
if err := f.enableCOPRRepos(ctx, coprPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to enable COPR repositories: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: System Packages (DNF)
|
||||
if len(dnfPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d system packages...", len(dnfPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(dnfPkgs, ", ")),
|
||||
}
|
||||
if err := f.installDNFPackages(ctx, dnfPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install DNF packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: COPR Packages
|
||||
coprPkgNames := f.extractPackageNames(coprPkgs)
|
||||
if len(coprPkgNames) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages, // Reusing AUR phase for COPR
|
||||
Progress: 0.65,
|
||||
Step: fmt.Sprintf("Installing %d COPR packages...", len(coprPkgNames)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing COPR packages: %s", strings.Join(coprPkgNames, ", ")),
|
||||
}
|
||||
if err := f.installCOPRPackages(ctx, coprPkgNames, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install COPR packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: Manual Builds
|
||||
if len(manualPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.85,
|
||||
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||
}
|
||||
if err := f.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 6: Configuration
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
|
||||
// Phase 7: Complete
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool) ([]string, []PackageMapping, []string) {
|
||||
dnfPkgs := []string{}
|
||||
coprPkgs := []PackageMapping{}
|
||||
manualPkgs := []string{}
|
||||
|
||||
variantMap := make(map[string]deps.PackageVariant)
|
||||
for _, dep := range dependencies {
|
||||
variantMap[dep.Name] = dep.Variant
|
||||
}
|
||||
|
||||
packageMap := f.GetPackageMappingWithVariants(wm, variantMap)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
// Skip installed packages unless marked for reinstall
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
f.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeSystem:
|
||||
dnfPkgs = append(dnfPkgs, pkgInfo.Name)
|
||||
case RepoTypeCOPR:
|
||||
coprPkgs = append(coprPkgs, pkgInfo)
|
||||
case RepoTypeManual:
|
||||
manualPkgs = append(manualPkgs, dep.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return dnfPkgs, coprPkgs, manualPkgs
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||
names := make([]string, len(packages))
|
||||
for i, pkg := range packages {
|
||||
names[i] = pkg.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
enabledRepos := make(map[string]bool)
|
||||
|
||||
for _, pkg := range coprPkgs {
|
||||
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||
f.log(fmt.Sprintf("Enabling COPR repository: %s", pkg.RepoURL))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.20,
|
||||
Step: fmt.Sprintf("Enabling COPR repo %s...", pkg.RepoURL),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logError(fmt.Sprintf("failed to enable COPR repo %s", pkg.RepoURL), err)
|
||||
f.log(fmt.Sprintf("COPR enable command output: %s", string(output)))
|
||||
return fmt.Errorf("failed to enable COPR repo %s: %w", pkg.RepoURL, err)
|
||||
}
|
||||
f.log(fmt.Sprintf("COPR repo %s enabled successfully: %s", pkg.RepoURL, string(output)))
|
||||
enabledRepos[pkg.RepoURL] = true
|
||||
|
||||
// Special handling for niri COPR repo - set priority=1
|
||||
if pkg.RepoURL == "yalter/niri-git" {
|
||||
f.log("Setting priority=1 for niri-git COPR repo...")
|
||||
repoFile := "/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:yalter:niri-git.repo"
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.22,
|
||||
Step: "Setting niri COPR repo priority...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
||||
}
|
||||
|
||||
priorityCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s' 2>&1", repoFile))
|
||||
priorityOutput, err := priorityCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logError("failed to set niri COPR repo priority", err)
|
||||
f.log(fmt.Sprintf("Priority command output: %s", string(priorityOutput)))
|
||||
return fmt.Errorf("failed to set niri COPR repo priority: %w", err)
|
||||
}
|
||||
f.log(fmt.Sprintf("niri COPR repo priority set successfully: %s", string(priorityOutput)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"dnf", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"dnf", "install", "-y"}
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg == "niri" || pkg == "niri-git" {
|
||||
args = append(args, "--setopt=install_weak_deps=False")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.70,
|
||||
Step: "Installing COPR packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||
}
|
||||
734
examples/danklinux/internal/distros/gentoo.go
Normal file
734
examples/danklinux/internal/distros/gentoo.go
Normal file
@@ -0,0 +1,734 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
var GentooGlobalUseFlags = []string{
|
||||
"X",
|
||||
"dbus",
|
||||
"udev",
|
||||
"alsa",
|
||||
"policykit",
|
||||
"jpeg",
|
||||
"png",
|
||||
"webp",
|
||||
"gif",
|
||||
"tiff",
|
||||
"svg",
|
||||
"brotli",
|
||||
"gdbm",
|
||||
"accessibility",
|
||||
"gtk",
|
||||
"qt6",
|
||||
"egl",
|
||||
"gbm",
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register("gentoo", "#54487A", FamilyGentoo, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewGentooDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type GentooDistribution struct {
|
||||
*BaseDistribution
|
||||
*ManualPackageInstaller
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewGentooDistribution(config DistroConfig, logChan chan<- string) *GentooDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &GentooDistribution{
|
||||
BaseDistribution: base,
|
||||
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getArchKeyword() string {
|
||||
arch := runtime.GOARCH
|
||||
switch arch {
|
||||
case "amd64":
|
||||
return "~amd64"
|
||||
case "arm64":
|
||||
return "~arm64"
|
||||
default:
|
||||
return "~amd64"
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) GetID() string {
|
||||
return g.config.ID
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) GetColorHex() string {
|
||||
return g.config.ColorHex
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) GetFamily() DistroFamily {
|
||||
return g.config.Family
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerPortage
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return g.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
dependencies = append(dependencies, g.detectDMS())
|
||||
|
||||
dependencies = append(dependencies, g.detectSpecificTerminal(terminal))
|
||||
|
||||
dependencies = append(dependencies, g.detectGit())
|
||||
dependencies = append(dependencies, g.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, g.detectQuickshell())
|
||||
dependencies = append(dependencies, g.detectXDGPortal())
|
||||
dependencies = append(dependencies, g.detectPolkitAgent())
|
||||
dependencies = append(dependencies, g.detectAccountsService())
|
||||
|
||||
if wm == deps.WindowManagerHyprland {
|
||||
dependencies = append(dependencies, g.detectHyprlandTools()...)
|
||||
}
|
||||
|
||||
if wm == deps.WindowManagerNiri {
|
||||
dependencies = append(dependencies, g.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
dependencies = append(dependencies, g.detectMatugen())
|
||||
dependencies = append(dependencies, g.detectDgop())
|
||||
dependencies = append(dependencies, g.detectHyprpicker())
|
||||
dependencies = append(dependencies, g.detectClipboardTools()...)
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) detectXDGPortal() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if g.packageInstalled("sys-apps/xdg-desktop-portal-gtk") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xdg-desktop-portal-gtk",
|
||||
Status: status,
|
||||
Description: "Desktop integration portal for GTK",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) detectPolkitAgent() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if g.packageInstalled("mate-extra/mate-polkit") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "mate-polkit",
|
||||
Status: status,
|
||||
Description: "PolicyKit authentication agent",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if g.packageInstalled("gui-apps/xwayland-satellite") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xwayland-satellite",
|
||||
Status: status,
|
||||
Description: "Xwayland support",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) detectAccountsService() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if g.packageInstalled("sys-apps/accountsservice") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "accountsservice",
|
||||
Status: status,
|
||||
Description: "D-Bus interface for user account query and manipulation",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("qlist", "-I", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
return g.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||
archKeyword := g.getArchKeyword()
|
||||
packages := map[string]PackageMapping{
|
||||
"git": {Name: "dev-vcs/git", Repository: RepoTypeSystem},
|
||||
"kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
||||
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
||||
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
||||
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
||||
"hyprpicker": g.getHyprpickerMapping(variants["hyprland"]),
|
||||
|
||||
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
|
||||
"qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
|
||||
"qtwayland": {Name: "dev-qt/qtwayland", Repository: RepoTypeSystem},
|
||||
"mesa": {Name: "media-libs/mesa", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
|
||||
|
||||
"quickshell": g.getQuickshellMapping(variants["quickshell"]),
|
||||
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||
"cliphist": {Name: "app-misc/cliphist", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||
"dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"])
|
||||
packages["grim"] = PackageMapping{Name: "gui-apps/grim", Repository: RepoTypeSystem}
|
||||
packages["slurp"] = PackageMapping{Name: "gui-apps/slurp", Repository: RepoTypeSystem}
|
||||
packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"])
|
||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||
packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerNiri:
|
||||
packages["niri"] = g.getNiriMapping(variants["niri"])
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "gui-apps/xwayland-satellite", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||
return PackageMapping{Name: "gui-apps/quickshell", Repository: RepoTypeGURU, UseFlags: "breakpad jemalloc sockets wayland layer-shell session-lock toplevel-management screencopy X pipewire tray mpris pam hyprland hyprland-global-shortcuts hyprland-focus-grab i3 i3-ipc bluetooth", AcceptKeywords: "**"}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping {
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
||||
archKeyword := g.getArchKeyword()
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeGURU, UseFlags: "X", AcceptKeywords: archKeyword}
|
||||
}
|
||||
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getHyprpickerMapping(_ deps.PackageVariant) PackageMapping {
|
||||
return PackageMapping{Name: "gui-apps/hyprpicker", Repository: RepoTypeGURU, AcceptKeywords: g.getArchKeyword()}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||
return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getPrerequisites() []string {
|
||||
return []string{
|
||||
"app-eselect/eselect-repository",
|
||||
"dev-vcs/git",
|
||||
"dev-build/make",
|
||||
"app-arch/unzip",
|
||||
"dev-util/pkgconf",
|
||||
"dev-qt/qtdeclarative",
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword string) error {
|
||||
useFlags := strings.Join(GentooGlobalUseFlags, " ")
|
||||
|
||||
checkCmd := exec.CommandContext(ctx, "grep", "-q", "^USE=", "/etc/portage/make.conf")
|
||||
hasUse := checkCmd.Run() == nil
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if hasUse {
|
||||
cmd = execSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf; exit_code=$?; exit $exit_code", useFlags))
|
||||
} else {
|
||||
cmd = execSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"; exit_code=$?; exit $exit_code", useFlags))
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
g.log(fmt.Sprintf("Failed to set global USE flags: %s", string(output)))
|
||||
return err
|
||||
}
|
||||
|
||||
g.log(fmt.Sprintf("Set global USE flags: %s", useFlags))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
prerequisites := g.getPrerequisites()
|
||||
var missingPkgs []string
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Setting global USE flags...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Configuring global USE flags in /etc/portage/make.conf",
|
||||
}
|
||||
|
||||
if err := g.setGlobalUseFlags(ctx, sudoPassword); err != nil {
|
||||
g.logError("failed to set global USE flags", err)
|
||||
return fmt.Errorf("failed to set global USE flags: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.06,
|
||||
Step: "Checking prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Checking prerequisite packages",
|
||||
}
|
||||
|
||||
for _, pkg := range prerequisites {
|
||||
checkCmd := exec.CommandContext(ctx, "qlist", "-I", pkg)
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
missingPkgs = append(missingPkgs, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
g.log("go not found in PATH, will install dev-lang/go")
|
||||
missingPkgs = append(missingPkgs, "dev-lang/go")
|
||||
} else {
|
||||
g.log("go already available in PATH")
|
||||
}
|
||||
|
||||
if len(missingPkgs) == 0 {
|
||||
g.log("All prerequisites already installed")
|
||||
return nil
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.07,
|
||||
Step: "Syncing Portage tree...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo emerge --sync",
|
||||
LogOutput: "Syncing Portage tree with emerge --sync",
|
||||
}
|
||||
|
||||
syncCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"emerge --sync --quiet; exit_code=$?; exit $exit_code")
|
||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||
if syncErr != nil {
|
||||
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
||||
return fmt.Errorf("failed to sync Portage tree: %w\nOutput: %s", syncErr, string(syncOutput))
|
||||
}
|
||||
g.log("Portage tree synced successfully")
|
||||
|
||||
g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.08,
|
||||
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo emerge --ask=n %s", strings.Join(missingPkgs, " ")),
|
||||
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, missingPkgs...)
|
||||
cmd := execSudoCommand(ctx, sudoPassword, fmt.Sprintf("%s; exit_code=$?; exit $exit_code", strings.Join(args, " ")))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
g.logError("failed to install prerequisites", err)
|
||||
g.log(fmt.Sprintf("Prerequisites command output: %s", string(output)))
|
||||
return fmt.Errorf("failed to install prerequisites: %w\nOutput: %s", err, string(output))
|
||||
}
|
||||
g.log(fmt.Sprintf("Prerequisites install output: %s", string(output)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := g.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
systemPkgs, guruPkgs, manualPkgs := g.categorizePackages(dependencies, wm, reinstallFlags)
|
||||
|
||||
g.log(fmt.Sprintf("CATEGORIZED PACKAGES: system=%d, guru=%d, manual=%d", len(systemPkgs), len(guruPkgs), len(manualPkgs)))
|
||||
|
||||
if len(systemPkgs) > 0 {
|
||||
systemPkgNames := g.extractPackageNames(systemPkgs)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgNames, ", ")),
|
||||
}
|
||||
if err := g.installPortagePackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Portage packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(guruPkgs) > 0 {
|
||||
g.log(fmt.Sprintf("FOUND %d GURU PACKAGES - WILL SYNC GURU REPO", len(guruPkgs)))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.60,
|
||||
Step: "Syncing GURU repository...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Syncing GURU repository to fetch latest ebuilds",
|
||||
}
|
||||
g.log("ABOUT TO CALL syncGURURepo")
|
||||
if err := g.syncGURURepo(ctx, sudoPassword, progressChan); err != nil {
|
||||
g.log(fmt.Sprintf("syncGURURepo RETURNED ERROR: %v", err))
|
||||
return fmt.Errorf("failed to sync GURU repository: %w", err)
|
||||
}
|
||||
g.log("syncGURURepo COMPLETED SUCCESSFULLY")
|
||||
|
||||
guruPkgNames := g.extractPackageNames(guruPkgs)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.65,
|
||||
Step: fmt.Sprintf("Installing %d GURU packages...", len(guruPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing GURU packages: %s", strings.Join(guruPkgNames, ", ")),
|
||||
}
|
||||
if err := g.installGURUPackages(ctx, guruPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install GURU packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(manualPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.85,
|
||||
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||
}
|
||||
if err := g.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool) ([]PackageMapping, []PackageMapping, []string) {
|
||||
systemPkgs := []PackageMapping{}
|
||||
guruPkgs := []PackageMapping{}
|
||||
manualPkgs := []string{}
|
||||
|
||||
variantMap := make(map[string]deps.PackageVariant)
|
||||
for _, dep := range dependencies {
|
||||
variantMap[dep.Name] = dep.Variant
|
||||
}
|
||||
|
||||
packageMap := g.GetPackageMappingWithVariants(wm, variantMap)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
g.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeSystem:
|
||||
systemPkgs = append(systemPkgs, pkgInfo)
|
||||
case RepoTypeGURU:
|
||||
guruPkgs = append(guruPkgs, pkgInfo)
|
||||
case RepoTypeManual:
|
||||
manualPkgs = append(manualPkgs, dep.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return systemPkgs, guruPkgs, manualPkgs
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||
names := make([]string, len(packages))
|
||||
for i, pkg := range packages {
|
||||
names[i] = pkg.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) installPortagePackages(ctx context.Context, packages []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
packageNames := g.extractPackageNames(packages)
|
||||
g.log(fmt.Sprintf("Installing Portage packages: %s", strings.Join(packageNames, ", ")))
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg.AcceptKeywords != "" {
|
||||
if err := g.setPackageAcceptKeywords(ctx, pkg.Name, pkg.AcceptKeywords, sudoPassword); err != nil {
|
||||
return fmt.Errorf("failed to set accept keywords for %s: %w", pkg.Name, err)
|
||||
}
|
||||
}
|
||||
if pkg.UseFlags != "" {
|
||||
if err := g.setPackageUseFlags(ctx, pkg.Name, pkg.UseFlags, sudoPassword); err != nil {
|
||||
return fmt.Errorf("failed to set USE flags for %s: %w", pkg.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, packageNames...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, fmt.Sprintf("%s || exit $?", strings.Join(args, " ")))
|
||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
||||
packageUseDir := "/etc/portage/package.use"
|
||||
|
||||
mkdirCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||
return fmt.Errorf("failed to create package.use directory: %w", err)
|
||||
}
|
||||
|
||||
useFlagLine := fmt.Sprintf("%s %s", packageName, useFlags)
|
||||
|
||||
checkExistingCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
fmt.Sprintf("grep -q '^%s ' %s/danklinux 2>/dev/null", packageName, packageUseDir))
|
||||
if checkExistingCmd.Run() == nil {
|
||||
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||
replaceCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||
return fmt.Errorf("failed to remove old USE flags: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
appendCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
||||
|
||||
output, err := appendCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
g.log(fmt.Sprintf("append output: %s", string(output)))
|
||||
return fmt.Errorf("failed to write USE flags to package.use: %w", err)
|
||||
}
|
||||
|
||||
g.log(fmt.Sprintf("Set USE flags for %s: %s", packageName, useFlags))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.55,
|
||||
Step: "Enabling GURU repository...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo eselect repository enable guru",
|
||||
LogOutput: "Enabling GURU repository with eselect",
|
||||
}
|
||||
|
||||
// Enable GURU repository
|
||||
enableCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
||||
output, err := enableCmd.CombinedOutput()
|
||||
|
||||
g.log(fmt.Sprintf("eselect repository enable guru output:\n%s", string(output)))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.55,
|
||||
LogOutput: "GURU repository enabled",
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.55,
|
||||
LogOutput: fmt.Sprintf("ERROR enabling GURU: %v", err),
|
||||
Error: err,
|
||||
}
|
||||
return fmt.Errorf("failed to enable GURU repository: %w\nOutput: %s", err, string(output))
|
||||
}
|
||||
|
||||
// Sync GURU repository
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.57,
|
||||
Step: "Syncing GURU repository...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo emaint sync --repo guru",
|
||||
LogOutput: "Syncing GURU repository",
|
||||
}
|
||||
|
||||
syncCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||
|
||||
g.log(fmt.Sprintf("emaint sync --repo guru output:\n%s", string(syncOutput)))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.57,
|
||||
LogOutput: "GURU repository synced",
|
||||
}
|
||||
|
||||
if syncErr != nil {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.57,
|
||||
LogOutput: fmt.Sprintf("ERROR syncing GURU: %v", syncErr),
|
||||
Error: syncErr,
|
||||
}
|
||||
return fmt.Errorf("failed to sync GURU repository: %w\nOutput: %s", syncErr, string(syncOutput))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packageName, keywords, sudoPassword string) error {
|
||||
checkCmd := exec.CommandContext(ctx, "portageq", "match", "/", packageName)
|
||||
if output, err := checkCmd.CombinedOutput(); err == nil && len(output) > 0 {
|
||||
g.log(fmt.Sprintf("Package %s is already available (may already be unmasked)", packageName))
|
||||
return nil
|
||||
}
|
||||
|
||||
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
||||
|
||||
mkdirCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||
return fmt.Errorf("failed to create package.accept_keywords directory: %w", err)
|
||||
}
|
||||
|
||||
keywordLine := fmt.Sprintf("%s %s", packageName, keywords)
|
||||
|
||||
checkExistingCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
fmt.Sprintf("grep -q '^%s ' %s/danklinux 2>/dev/null", packageName, acceptKeywordsDir))
|
||||
if checkExistingCmd.Run() == nil {
|
||||
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||
replaceCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||
return fmt.Errorf("failed to remove old accept keywords: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
appendCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
||||
|
||||
output, err := appendCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
g.log(fmt.Sprintf("append output: %s", string(output)))
|
||||
return fmt.Errorf("failed to write accept keywords: %w", err)
|
||||
}
|
||||
|
||||
g.log(fmt.Sprintf("Set accept keywords for %s: %s", packageName, keywords))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
packageNames := g.extractPackageNames(packages)
|
||||
g.log(fmt.Sprintf("Installing GURU packages: %s", strings.Join(packageNames, ", ")))
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg.AcceptKeywords != "" {
|
||||
if err := g.setPackageAcceptKeywords(ctx, pkg.Name, pkg.AcceptKeywords, sudoPassword); err != nil {
|
||||
return fmt.Errorf("failed to set accept keywords for %s: %w", pkg.Name, err)
|
||||
}
|
||||
}
|
||||
if pkg.UseFlags != "" {
|
||||
if err := g.setPackageUseFlags(ctx, pkg.Name, pkg.UseFlags, sudoPassword); err != nil {
|
||||
return fmt.Errorf("failed to set USE flags for %s: %w", pkg.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guruPackages := make([]string, len(packageNames))
|
||||
for i, pkg := range packageNames {
|
||||
guruPackages[i] = pkg + "::guru"
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, guruPackages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.70,
|
||||
Step: "Installing GURU packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, fmt.Sprintf("%s || exit $?", strings.Join(args, " ")))
|
||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
||||
}
|
||||
156
examples/danklinux/internal/distros/interface.go
Normal file
156
examples/danklinux/internal/distros/interface.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
// DistroFamily represents a family of related distributions
|
||||
type DistroFamily string
|
||||
|
||||
const (
|
||||
FamilyArch DistroFamily = "arch"
|
||||
FamilyFedora DistroFamily = "fedora"
|
||||
FamilySUSE DistroFamily = "suse"
|
||||
FamilyUbuntu DistroFamily = "ubuntu"
|
||||
FamilyDebian DistroFamily = "debian"
|
||||
FamilyNix DistroFamily = "nix"
|
||||
FamilyGentoo DistroFamily = "gentoo"
|
||||
)
|
||||
|
||||
// PackageManagerType defines the package manager a distro uses
|
||||
type PackageManagerType string
|
||||
|
||||
const (
|
||||
PackageManagerPacman PackageManagerType = "pacman"
|
||||
PackageManagerDNF PackageManagerType = "dnf"
|
||||
PackageManagerAPT PackageManagerType = "apt"
|
||||
PackageManagerZypper PackageManagerType = "zypper"
|
||||
PackageManagerNix PackageManagerType = "nix"
|
||||
PackageManagerPortage PackageManagerType = "portage"
|
||||
)
|
||||
|
||||
// RepositoryType defines the type of repository for a package
|
||||
type RepositoryType string
|
||||
|
||||
const (
|
||||
RepoTypeSystem RepositoryType = "system" // Standard system repo (pacman, dnf, apt)
|
||||
RepoTypeAUR RepositoryType = "aur" // Arch User Repository
|
||||
RepoTypeCOPR RepositoryType = "copr" // Fedora COPR
|
||||
RepoTypePPA RepositoryType = "ppa" // Ubuntu PPA
|
||||
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
||||
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
||||
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
||||
)
|
||||
|
||||
// InstallPhase represents the current phase of installation
|
||||
type InstallPhase int
|
||||
|
||||
const (
|
||||
PhasePrerequisites InstallPhase = iota
|
||||
PhaseAURHelper
|
||||
PhaseSystemPackages
|
||||
PhaseAURPackages
|
||||
PhaseCursorTheme
|
||||
PhaseConfiguration
|
||||
PhaseComplete
|
||||
)
|
||||
|
||||
// InstallProgressMsg represents progress during package installation
|
||||
type InstallProgressMsg struct {
|
||||
Phase InstallPhase
|
||||
Progress float64
|
||||
Step string
|
||||
IsComplete bool
|
||||
NeedsSudo bool
|
||||
CommandInfo string
|
||||
LogOutput string
|
||||
Error error
|
||||
}
|
||||
|
||||
// PackageMapping defines how to install a package on a specific distro
|
||||
type PackageMapping struct {
|
||||
Name string // Package name to install
|
||||
Repository RepositoryType // Repository type
|
||||
RepoURL string // Repository URL if needed (e.g., COPR repo, PPA)
|
||||
BuildFunc string // Name of manual build function if RepoTypeManual
|
||||
UseFlags string // USE flags for Gentoo packages
|
||||
AcceptKeywords string // Accept keywords for Gentoo packages (e.g., "~amd64")
|
||||
}
|
||||
|
||||
// Distribution defines a Linux distribution with all its specific configurations
|
||||
type Distribution interface {
|
||||
// Metadata
|
||||
GetID() string
|
||||
GetColorHex() string
|
||||
GetFamily() DistroFamily
|
||||
GetPackageManager() PackageManagerType
|
||||
|
||||
// Dependency Detection
|
||||
DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error)
|
||||
DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error)
|
||||
|
||||
// Package Installation
|
||||
InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error
|
||||
|
||||
// Package Mapping
|
||||
GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping
|
||||
|
||||
// Prerequisites
|
||||
InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error
|
||||
}
|
||||
|
||||
// DistroConfig holds configuration for a distribution
|
||||
type DistroConfig struct {
|
||||
ID string
|
||||
ColorHex string
|
||||
Family DistroFamily
|
||||
Constructor func(config DistroConfig, logChan chan<- string) Distribution
|
||||
}
|
||||
|
||||
// Registry holds all supported distributions
|
||||
var Registry = make(map[string]DistroConfig)
|
||||
|
||||
// Register adds a distribution to the registry
|
||||
func Register(id, colorHex string, family DistroFamily, constructor func(config DistroConfig, logChan chan<- string) Distribution) {
|
||||
Registry[id] = DistroConfig{
|
||||
ID: id,
|
||||
ColorHex: colorHex,
|
||||
Family: family,
|
||||
Constructor: constructor,
|
||||
}
|
||||
}
|
||||
|
||||
// GetSupportedDistros returns a list of all supported distribution IDs
|
||||
func GetSupportedDistros() []string {
|
||||
ids := make([]string, 0, len(Registry))
|
||||
for id := range Registry {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// IsDistroSupported checks if a distribution ID is supported
|
||||
func IsDistroSupported(id string) bool {
|
||||
_, exists := Registry[id]
|
||||
return exists
|
||||
}
|
||||
|
||||
// NewDistribution creates a distribution instance by ID
|
||||
func NewDistribution(id string, logChan chan<- string) (Distribution, error) {
|
||||
config, exists := Registry[id]
|
||||
if !exists {
|
||||
return nil, &UnsupportedDistributionError{ID: id}
|
||||
}
|
||||
return config.Constructor(config, logChan), nil
|
||||
}
|
||||
|
||||
// UnsupportedDistributionError is returned when a distribution is not supported
|
||||
type UnsupportedDistributionError struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
func (e *UnsupportedDistributionError) Error() string {
|
||||
return "unsupported distribution: " + e.ID
|
||||
}
|
||||
802
examples/danklinux/internal/distros/manual_packages.go
Normal file
802
examples/danklinux/internal/distros/manual_packages.go
Normal file
@@ -0,0 +1,802 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ManualPackageInstaller provides methods for installing packages from source
|
||||
type ManualPackageInstaller struct {
|
||||
*BaseDistribution
|
||||
}
|
||||
|
||||
// parseLatestTagFromGitOutput parses git ls-remote output and returns the latest tag
|
||||
func (m *ManualPackageInstaller) parseLatestTagFromGitOutput(output string) string {
|
||||
lines := strings.Split(output, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "refs/tags/") && !strings.Contains(line, "^{}") {
|
||||
parts := strings.Split(line, "refs/tags/")
|
||||
if len(parts) > 1 {
|
||||
latestTag := strings.TrimSpace(parts[1])
|
||||
return latestTag
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getLatestQuickshellTag fetches the latest tag from the quickshell repository
|
||||
func (m *ManualPackageInstaller) getLatestQuickshellTag(ctx context.Context) string {
|
||||
tagCmd := exec.CommandContext(ctx, "git", "ls-remote", "--tags", "--sort=-v:refname",
|
||||
"https://github.com/quickshell-mirror/quickshell.git")
|
||||
tagOutput, err := tagCmd.Output()
|
||||
if err != nil {
|
||||
m.log(fmt.Sprintf("Warning: failed to fetch quickshell tags: %v", err))
|
||||
return ""
|
||||
}
|
||||
|
||||
return m.parseLatestTagFromGitOutput(string(tagOutput))
|
||||
}
|
||||
|
||||
// InstallManualPackages handles packages that need manual building
|
||||
func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
for _, pkg := range packages {
|
||||
switch pkg {
|
||||
case "dms (DankMaterialShell)", "dms":
|
||||
if err := m.installDankMaterialShell(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install DankMaterialShell: %w", err)
|
||||
}
|
||||
case "dgop":
|
||||
if err := m.installDgop(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install dgop: %w", err)
|
||||
}
|
||||
case "grimblast":
|
||||
if err := m.installGrimblast(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install grimblast: %w", err)
|
||||
}
|
||||
case "niri":
|
||||
if err := m.installNiri(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install niri: %w", err)
|
||||
}
|
||||
case "quickshell":
|
||||
if err := m.installQuickshell(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||
}
|
||||
case "hyprland":
|
||||
if err := m.installHyprland(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install hyprland: %w", err)
|
||||
}
|
||||
case "hyprpicker":
|
||||
if err := m.installHyprpicker(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
||||
}
|
||||
case "ghostty":
|
||||
if err := m.installGhostty(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||
}
|
||||
case "matugen":
|
||||
if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install matugen: %w", err)
|
||||
}
|
||||
case "cliphist":
|
||||
if err := m.installCliphist(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install cliphist: %w", err)
|
||||
}
|
||||
case "xwayland-satellite":
|
||||
if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
|
||||
}
|
||||
default:
|
||||
m.log(fmt.Sprintf("Warning: No manual build method for %s", pkg))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing dgop from source...")
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return fmt.Errorf("HOME environment variable not set")
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := filepath.Join(cacheDir, "dgop-build")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Cloning dgop repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/AvengeMedia/dgop.git",
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/AvengeMedia/dgop.git", tmpDir)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
m.logError("failed to clone dgop repository", err)
|
||||
return fmt.Errorf("failed to clone dgop repository: %w", err)
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "make")
|
||||
buildCmd.Dir = tmpDir
|
||||
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.7, "Building dgop..."); err != nil {
|
||||
return fmt.Errorf("failed to build dgop: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.7,
|
||||
Step: "Installing dgop...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo make install",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword, "make install")
|
||||
installCmd.Dir = tmpDir
|
||||
if err := installCmd.Run(); err != nil {
|
||||
m.logError("failed to install dgop", err)
|
||||
return fmt.Errorf("failed to install dgop: %w", err)
|
||||
}
|
||||
|
||||
m.log("dgop installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installGrimblast(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing grimblast script for Hyprland...")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Downloading grimblast script...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "curl grimblast script",
|
||||
}
|
||||
|
||||
grimblastURL := "https://raw.githubusercontent.com/hyprwm/contrib/refs/heads/main/grimblast/grimblast"
|
||||
tmpPath := filepath.Join(os.TempDir(), "grimblast")
|
||||
|
||||
downloadCmd := exec.CommandContext(ctx, "curl", "-L", "-o", tmpPath, grimblastURL)
|
||||
if err := downloadCmd.Run(); err != nil {
|
||||
m.logError("failed to download grimblast", err)
|
||||
return fmt.Errorf("failed to download grimblast: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.5,
|
||||
Step: "Making grimblast executable...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "chmod +x grimblast",
|
||||
}
|
||||
|
||||
chmodCmd := exec.CommandContext(ctx, "chmod", "+x", tmpPath)
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
m.logError("failed to make grimblast executable", err)
|
||||
return fmt.Errorf("failed to make grimblast executable: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.8,
|
||||
Step: "Installing grimblast to /usr/local/bin...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo cp grimblast /usr/local/bin/",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cp %s /usr/local/bin/grimblast", tmpPath))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
m.logError("failed to install grimblast", err)
|
||||
return fmt.Errorf("failed to install grimblast: %w", err)
|
||||
}
|
||||
|
||||
os.Remove(tmpPath)
|
||||
|
||||
m.log("grimblast installed successfully to /usr/local/bin")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing niri from source...")
|
||||
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "niri-build")
|
||||
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "tmp")
|
||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create build directory: %w", err)
|
||||
}
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
os.RemoveAll(buildDir)
|
||||
os.RemoveAll(tmpDir)
|
||||
}()
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.2,
|
||||
Step: "Cloning niri repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/YaLTeR/niri.git",
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/YaLTeR/niri.git", buildDir)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone niri: %w", err)
|
||||
}
|
||||
|
||||
checkoutCmd := exec.CommandContext(ctx, "git", "-C", buildDir, "checkout", "v25.08")
|
||||
if err := checkoutCmd.Run(); err != nil {
|
||||
m.log(fmt.Sprintf("Warning: failed to checkout v25.08, using main: %v", err))
|
||||
}
|
||||
|
||||
if !m.commandExists("cargo-deb") {
|
||||
cargoDebInstallCmd := exec.CommandContext(ctx, "cargo", "install", "cargo-deb")
|
||||
cargoDebInstallCmd.Env = append(os.Environ(), "TMPDIR="+tmpDir)
|
||||
if err := m.runWithProgressStep(cargoDebInstallCmd, progressChan, PhaseSystemPackages, 0.3, 0.35, "Installing cargo-deb..."); err != nil {
|
||||
return fmt.Errorf("failed to install cargo-deb: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
buildDebCmd := exec.CommandContext(ctx, "cargo", "deb")
|
||||
buildDebCmd.Dir = buildDir
|
||||
buildDebCmd.Env = append(os.Environ(), "TMPDIR="+tmpDir)
|
||||
if err := m.runWithProgressStep(buildDebCmd, progressChan, PhaseSystemPackages, 0.35, 0.95, "Building niri deb package..."); err != nil {
|
||||
return fmt.Errorf("failed to build niri deb: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.95,
|
||||
Step: "Installing niri deb package...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "dpkg -i niri.deb",
|
||||
}
|
||||
|
||||
installDebCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
||||
|
||||
output, err := installDebCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
m.log(fmt.Sprintf("dpkg install failed. Output:\n%s", string(output)))
|
||||
return fmt.Errorf("failed to install niri deb package: %w\nOutput:\n%s", err, string(output))
|
||||
}
|
||||
|
||||
m.log(fmt.Sprintf("dpkg install successful. Output:\n%s", string(output)))
|
||||
|
||||
m.log("niri installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing quickshell from source...")
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return fmt.Errorf("HOME environment variable not set")
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Cloning quickshell repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/quickshell-mirror/quickshell.git",
|
||||
}
|
||||
|
||||
var cloneCmd *exec.Cmd
|
||||
if forceQuickshellGit {
|
||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||
} else {
|
||||
// Get latest tag from repository
|
||||
latestTag := m.getLatestQuickshellTag(ctx)
|
||||
if latestTag != "" {
|
||||
m.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag))
|
||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "--branch", latestTag, "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||
} else {
|
||||
m.log("Warning: failed to fetch latest tag, using default branch")
|
||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||
}
|
||||
}
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone quickshell: %w", err)
|
||||
}
|
||||
|
||||
buildDir := tmpDir + "/build"
|
||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create build directory: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.3,
|
||||
Step: "Configuring quickshell build...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "cmake -B build -S . -G Ninja",
|
||||
}
|
||||
|
||||
configureCmd := exec.CommandContext(ctx, "cmake", "-GNinja", "-B", "build",
|
||||
"-DCMAKE_BUILD_TYPE=RelWithDebInfo",
|
||||
"-DCRASH_REPORTER=off",
|
||||
"-DCMAKE_CXX_STANDARD=20")
|
||||
configureCmd.Dir = tmpDir
|
||||
configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||
|
||||
output, err := configureCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
m.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
|
||||
return fmt.Errorf("failed to configure quickshell: %w\nCMake output:\n%s", err, string(output))
|
||||
}
|
||||
|
||||
m.log(fmt.Sprintf("cmake configure successful. Output:\n%s", string(output)))
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.4,
|
||||
Step: "Building quickshell (this may take a while)...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "cmake --build build",
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "build")
|
||||
buildCmd.Dir = tmpDir
|
||||
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.8, "Building quickshell..."); err != nil {
|
||||
return fmt.Errorf("failed to build quickshell: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.8,
|
||||
Step: "Installing quickshell...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo cmake --install build",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cd %s && cmake --install build", tmpDir))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||
}
|
||||
|
||||
m.log("quickshell installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing Hyprland from source...")
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return fmt.Errorf("HOME environment variable not set")
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := filepath.Join(cacheDir, "hyprland-build")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Cloning Hyprland repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone --recursive https://github.com/hyprwm/Hyprland.git",
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "--recursive", "https://github.com/hyprwm/Hyprland.git", tmpDir)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone Hyprland: %w", err)
|
||||
}
|
||||
|
||||
checkoutCmd := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", "v0.50.1")
|
||||
if err := checkoutCmd.Run(); err != nil {
|
||||
m.log(fmt.Sprintf("Warning: failed to checkout v0.50.1, using main: %v", err))
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "make", "all")
|
||||
buildCmd.Dir = tmpDir
|
||||
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.2, 0.8, "Building Hyprland..."); err != nil {
|
||||
return fmt.Errorf("failed to build Hyprland: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.8,
|
||||
Step: "Installing Hyprland...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo make install",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cd %s && make install", tmpDir))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install Hyprland: %w", err)
|
||||
}
|
||||
|
||||
m.log("Hyprland installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing hyprpicker from source...")
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return fmt.Errorf("HOME environment variable not set")
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := filepath.Join(cacheDir, "hyprpicker-build")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.2,
|
||||
Step: "Cloning hyprpicker repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprpicker.git", tmpDir)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone hyprpicker: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.4,
|
||||
Step: "Building hyprpicker...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "make all",
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "make", "all")
|
||||
buildCmd.Dir = tmpDir
|
||||
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||
if err := buildCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to build hyprpicker: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.8,
|
||||
Step: "Installing hyprpicker...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo make install",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cd %s && make install", tmpDir))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
||||
}
|
||||
|
||||
m.log("hyprpicker installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing Ghostty from source...")
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return fmt.Errorf("HOME environment variable not set")
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := filepath.Join(cacheDir, "ghostty-build")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Cloning Ghostty repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/ghostty-org/ghostty.git",
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/ghostty-org/ghostty.git", tmpDir)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone Ghostty: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.2,
|
||||
Step: "Building Ghostty (this may take a while)...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "zig build -Doptimize=ReleaseFast",
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "zig", "build", "-Doptimize=ReleaseFast")
|
||||
buildCmd.Dir = tmpDir
|
||||
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||
if err := buildCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to build Ghostty: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.8,
|
||||
Step: "Installing Ghostty...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||
}
|
||||
|
||||
m.log("Ghostty installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing matugen from source...")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Installing matugen via cargo...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "cargo install matugen",
|
||||
}
|
||||
|
||||
installCmd := exec.CommandContext(ctx, "cargo", "install", "matugen")
|
||||
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building matugen..."); err != nil {
|
||||
return fmt.Errorf("failed to install matugen: %w", err)
|
||||
}
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
sourcePath := filepath.Join(homeDir, ".cargo", "bin", "matugen")
|
||||
targetPath := "/usr/local/bin/matugen"
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.7,
|
||||
Step: "Installing matugen binary to system...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||
}
|
||||
|
||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := copyCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
||||
}
|
||||
|
||||
// Make it executable
|
||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to make matugen executable: %w", err)
|
||||
}
|
||||
|
||||
m.log("matugen installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing DankMaterialShell (DMS)...")
|
||||
|
||||
// Always install/update the DMS binary
|
||||
if err := m.installDMSBinary(ctx, sudoPassword, progressChan); err != nil {
|
||||
m.logError("Failed to install DMS binary", err)
|
||||
}
|
||||
|
||||
// Handle DMS config - clone if missing, pull if exists
|
||||
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
|
||||
if _, err := os.Stat(dmsPath); os.IsNotExist(err) {
|
||||
// Config doesn't exist, clone it
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.90,
|
||||
Step: "Cloning DankMaterialShell config...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git ~/.config/quickshell/dms",
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(dmsPath)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone",
|
||||
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
|
||||
}
|
||||
|
||||
if !forceDMSGit {
|
||||
fetchCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "fetch", "--tags")
|
||||
if err := fetchCmd.Run(); err == nil {
|
||||
tagCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "describe", "--tags", "--abbrev=0", "origin/master")
|
||||
if tagOutput, err := tagCmd.Output(); err == nil {
|
||||
latestTag := strings.TrimSpace(string(tagOutput))
|
||||
checkoutCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "checkout", latestTag)
|
||||
if err := checkoutCmd.Run(); err == nil {
|
||||
m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.log("DankMaterialShell config cloned successfully")
|
||||
} else {
|
||||
// Config exists, update it
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.90,
|
||||
Step: "Updating DankMaterialShell config...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git pull in ~/.config/quickshell/dms",
|
||||
}
|
||||
|
||||
pullCmd := exec.CommandContext(ctx, "git", "pull")
|
||||
pullCmd.Dir = dmsPath
|
||||
if err := pullCmd.Run(); err != nil {
|
||||
m.logError("Failed to update DankMaterialShell config", err)
|
||||
} else {
|
||||
m.log("DankMaterialShell config updated successfully")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installCliphist(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing cliphist from source...")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Installing cliphist via go install...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "go install go.senan.xyz/cliphist@latest",
|
||||
}
|
||||
|
||||
installCmd := exec.CommandContext(ctx, "go", "install", "go.senan.xyz/cliphist@latest")
|
||||
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building cliphist..."); err != nil {
|
||||
return fmt.Errorf("failed to install cliphist: %w", err)
|
||||
}
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
sourcePath := filepath.Join(homeDir, "go", "bin", "cliphist")
|
||||
targetPath := "/usr/local/bin/cliphist"
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.7,
|
||||
Step: "Installing cliphist binary to system...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||
}
|
||||
|
||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := copyCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to copy cliphist to /usr/local/bin: %w", err)
|
||||
}
|
||||
|
||||
// Make it executable
|
||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to make cliphist executable: %w", err)
|
||||
}
|
||||
|
||||
m.log("cliphist installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing xwayland-satellite from source...")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Installing xwayland-satellite via cargo...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "cargo install --git https://github.com/Supreeeme/xwayland-satellite --tag v0.7",
|
||||
}
|
||||
|
||||
installCmd := exec.CommandContext(ctx, "cargo", "install", "--git", "https://github.com/Supreeeme/xwayland-satellite", "--tag", "v0.7")
|
||||
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building xwayland-satellite..."); err != nil {
|
||||
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
|
||||
}
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
sourcePath := filepath.Join(homeDir, ".cargo", "bin", "xwayland-satellite")
|
||||
targetPath := "/usr/local/bin/xwayland-satellite"
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.7,
|
||||
Step: "Installing xwayland-satellite binary to system...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||
}
|
||||
|
||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := copyCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
||||
}
|
||||
|
||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
||||
}
|
||||
|
||||
m.log("xwayland-satellite installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
122
examples/danklinux/internal/distros/manual_packages_test.go
Normal file
122
examples/danklinux/internal/distros/manual_packages_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestManualPackageInstaller_parseLatestTagFromGitOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "normal tag output",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0
|
||||
703a3789083d2f990c4e99cd25c97c2a4cccbd81 refs/tags/v0.1.0`,
|
||||
expected: "v0.2.1",
|
||||
},
|
||||
{
|
||||
name: "annotated tags with ^{}",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1
|
||||
b1b150fab00a93ea983aaca5df55304bc837f51c refs/tags/v0.2.1^{}
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`,
|
||||
expected: "v0.2.1",
|
||||
},
|
||||
{
|
||||
name: "mixed tags",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.3.0
|
||||
b1b150fab00a93ea983aaca5df55304bc837f51c refs/tags/v0.3.0^{}
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0
|
||||
c1c150fab00a93ea983aaca5df55304bc837f51d refs/tags/beta-1`,
|
||||
expected: "v0.3.0",
|
||||
},
|
||||
{
|
||||
name: "empty output",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "no tags",
|
||||
input: "some other output\nwithout tags",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "only annotated tags",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1^{}
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0^{}`,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "single tag",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v1.0.0`,
|
||||
expected: "v1.0.0",
|
||||
},
|
||||
{
|
||||
name: "tag with extra whitespace",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`,
|
||||
expected: "v0.2.1",
|
||||
},
|
||||
{
|
||||
name: "beta and rc tags",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.3.0-beta.1
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`,
|
||||
expected: "v0.3.0-beta.1",
|
||||
},
|
||||
{
|
||||
name: "tags without v prefix",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/0.2.1
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/0.2.0`,
|
||||
expected: "0.2.1",
|
||||
},
|
||||
{
|
||||
name: "multiple lines with spaces",
|
||||
input: `
|
||||
a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v1.2.3
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v1.2.2
|
||||
`,
|
||||
expected: "v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "tag at end of line",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1`,
|
||||
expected: "v0.2.1",
|
||||
},
|
||||
}
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
installer := &ManualPackageInstaller{BaseDistribution: base}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := installer.parseLatestTagFromGitOutput(tt.input)
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("parseLatestTagFromGitOutput() = %q, expected %q", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualPackageInstaller_parseLatestTagFromGitOutput_EmptyInstaller(t *testing.T) {
|
||||
// Test that parsing works even with a minimal installer setup
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
installer := &ManualPackageInstaller{BaseDistribution: base}
|
||||
|
||||
input := `abc123 refs/tags/v1.0.0
|
||||
def456 refs/tags/v0.9.0`
|
||||
|
||||
result := installer.parseLatestTagFromGitOutput(input)
|
||||
|
||||
if result != "v1.0.0" {
|
||||
t.Errorf("Expected v1.0.0, got %s", result)
|
||||
}
|
||||
}
|
||||
458
examples/danklinux/internal/distros/nixos.go
Normal file
458
examples/danklinux/internal/distros/nixos.go
Normal file
@@ -0,0 +1,458 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("nixos", "#7EBAE4", FamilyNix, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewNixOSDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type NixOSDistribution struct {
|
||||
*BaseDistribution
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewNixOSDistribution(config DistroConfig, logChan chan<- string) *NixOSDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &NixOSDistribution{
|
||||
BaseDistribution: base,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) GetID() string {
|
||||
return n.config.ID
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) GetColorHex() string {
|
||||
return n.config.ColorHex
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) GetFamily() DistroFamily {
|
||||
return n.config.Family
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerNix
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return n.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
// DMS at the top (shell is prominent)
|
||||
dependencies = append(dependencies, n.detectDMS())
|
||||
|
||||
// Terminal with choice support
|
||||
dependencies = append(dependencies, n.detectSpecificTerminal(terminal))
|
||||
|
||||
// Common detections using base methods
|
||||
dependencies = append(dependencies, n.detectGit())
|
||||
dependencies = append(dependencies, n.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, n.detectQuickshell())
|
||||
dependencies = append(dependencies, n.detectXDGPortal())
|
||||
dependencies = append(dependencies, n.detectPolkitAgent())
|
||||
dependencies = append(dependencies, n.detectAccountsService())
|
||||
|
||||
// Hyprland-specific tools
|
||||
if wm == deps.WindowManagerHyprland {
|
||||
dependencies = append(dependencies, n.detectHyprlandTools()...)
|
||||
}
|
||||
|
||||
// Niri-specific tools
|
||||
if wm == deps.WindowManagerNiri {
|
||||
dependencies = append(dependencies, n.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
// Base detections (common across distros)
|
||||
dependencies = append(dependencies, n.detectMatugen())
|
||||
dependencies = append(dependencies, n.detectDgop())
|
||||
dependencies = append(dependencies, n.detectHyprpicker())
|
||||
dependencies = append(dependencies, n.detectClipboardTools()...)
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectDMS() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
|
||||
// For NixOS, check if quickshell can find the dms config
|
||||
cmd := exec.Command("qs", "-c", "dms", "--list")
|
||||
if err := cmd.Run(); err == nil {
|
||||
status = deps.StatusInstalled
|
||||
} else if n.packageInstalled("DankMaterialShell") {
|
||||
// Fallback: check if flake is in profile
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "dms (DankMaterialShell)",
|
||||
Status: status,
|
||||
Description: "Desktop Management System configuration (installed as flake)",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectXDGPortal() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if n.packageInstalled("xdg-desktop-portal-gtk") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xdg-desktop-portal-gtk",
|
||||
Status: status,
|
||||
Description: "Desktop integration portal for GTK",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectWindowManager(wm deps.WindowManager) deps.Dependency {
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
status := deps.StatusMissing
|
||||
description := "Dynamic tiling Wayland compositor"
|
||||
if n.commandExists("hyprland") || n.commandExists("Hyprland") {
|
||||
status = deps.StatusInstalled
|
||||
} else {
|
||||
description = "Install system-wide: programs.hyprland.enable = true; in configuration.nix"
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "hyprland",
|
||||
Status: status,
|
||||
Description: description,
|
||||
Required: true,
|
||||
}
|
||||
case deps.WindowManagerNiri:
|
||||
status := deps.StatusMissing
|
||||
description := "Scrollable-tiling Wayland compositor"
|
||||
if n.commandExists("niri") {
|
||||
status = deps.StatusInstalled
|
||||
} else {
|
||||
description = "Install system-wide: environment.systemPackages = [ pkgs.niri ]; in configuration.nix"
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "niri",
|
||||
Status: status,
|
||||
Description: description,
|
||||
Required: true,
|
||||
}
|
||||
default:
|
||||
return deps.Dependency{
|
||||
Name: "unknown-wm",
|
||||
Status: deps.StatusMissing,
|
||||
Description: "Unknown window manager",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectHyprlandTools() []deps.Dependency {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
tools := []struct {
|
||||
name string
|
||||
description string
|
||||
}{
|
||||
{"grim", "Screenshot utility for Wayland"},
|
||||
{"slurp", "Region selection utility for Wayland"},
|
||||
{"hyprctl", "Hyprland control utility (comes with system Hyprland)"},
|
||||
{"hyprpicker", "Color picker for Hyprland"},
|
||||
{"grimblast", "Screenshot script for Hyprland"},
|
||||
{"jq", "JSON processor"},
|
||||
}
|
||||
|
||||
for _, tool := range tools {
|
||||
status := deps.StatusMissing
|
||||
|
||||
// Special handling for hyprctl - it comes with system hyprland
|
||||
if tool.name == "hyprctl" {
|
||||
if n.commandExists("hyprctl") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
} else {
|
||||
if n.commandExists(tool.name) {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
}
|
||||
|
||||
dependencies = append(dependencies, deps.Dependency{
|
||||
Name: tool.name,
|
||||
Status: status,
|
||||
Description: tool.description,
|
||||
Required: true,
|
||||
})
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if n.commandExists("xwayland-satellite") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xwayland-satellite",
|
||||
Status: status,
|
||||
Description: "Xwayland support",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectPolkitAgent() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if n.packageInstalled("mate-polkit") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "mate-polkit",
|
||||
Status: status,
|
||||
Description: "PolicyKit authentication agent",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectAccountsService() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if n.packageInstalled("accountsservice") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "accountsservice",
|
||||
Status: status,
|
||||
Description: "D-Bus interface for user account query and manipulation",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("nix", "profile", "list")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(output), pkg)
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
packages := map[string]PackageMapping{
|
||||
"git": {Name: "nixpkgs#git", Repository: RepoTypeSystem},
|
||||
"quickshell": {Name: "github:quickshell-mirror/quickshell", Repository: RepoTypeFlake},
|
||||
"matugen": {Name: "github:InioX/matugen", Repository: RepoTypeFlake},
|
||||
"dgop": {Name: "github:AvengeMedia/dgop", Repository: RepoTypeFlake},
|
||||
"dms (DankMaterialShell)": {Name: "github:AvengeMedia/DankMaterialShell", Repository: RepoTypeFlake},
|
||||
"ghostty": {Name: "nixpkgs#ghostty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "nixpkgs#alacritty", Repository: RepoTypeSystem},
|
||||
"cliphist": {Name: "nixpkgs#cliphist", Repository: RepoTypeSystem},
|
||||
"wl-clipboard": {Name: "nixpkgs#wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "nixpkgs#xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"mate-polkit": {Name: "nixpkgs#mate.mate-polkit", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "nixpkgs#accountsservice", Repository: RepoTypeSystem},
|
||||
"hyprpicker": {Name: "nixpkgs#hyprpicker", Repository: RepoTypeSystem},
|
||||
}
|
||||
|
||||
// Note: Window managers (hyprland/niri) should be installed system-wide on NixOS
|
||||
// We only install the tools here
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
// Skip hyprland itself - should be installed system-wide
|
||||
packages["grim"] = PackageMapping{Name: "nixpkgs#grim", Repository: RepoTypeSystem}
|
||||
packages["slurp"] = PackageMapping{Name: "nixpkgs#slurp", Repository: RepoTypeSystem}
|
||||
packages["grimblast"] = PackageMapping{Name: "github:hyprwm/contrib#grimblast", Repository: RepoTypeFlake}
|
||||
packages["jq"] = PackageMapping{Name: "nixpkgs#jq", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerNiri:
|
||||
// Skip niri itself - should be installed system-wide
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "nixpkgs#xwayland-satellite", Repository: RepoTypeFlake}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.10,
|
||||
Step: "NixOS prerequisites ready",
|
||||
IsComplete: false,
|
||||
LogOutput: "NixOS package manager is ready to use",
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error {
|
||||
// Phase 1: Check Prerequisites
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := n.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
nixpkgsPkgs, flakePkgs := n.categorizePackages(dependencies, wm, reinstallFlags)
|
||||
|
||||
// Phase 2: Nixpkgs Packages
|
||||
if len(nixpkgsPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d packages from nixpkgs...", len(nixpkgsPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(nixpkgsPkgs, ", ")),
|
||||
}
|
||||
if err := n.installNixpkgsPackages(ctx, nixpkgsPkgs, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install nixpkgs packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Flake Packages
|
||||
if len(flakePkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.65,
|
||||
Step: fmt.Sprintf("Installing %d packages from flakes...", len(flakePkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing flake packages: %s", strings.Join(flakePkgs, ", ")),
|
||||
}
|
||||
if err := n.installFlakePackages(ctx, flakePkgs, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install flake packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: Configuration
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
if err := n.postInstallConfig(progressChan); err != nil {
|
||||
return fmt.Errorf("failed to configure system: %w", err)
|
||||
}
|
||||
|
||||
// Phase 5: Complete
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool) ([]string, []string) {
|
||||
nixpkgsPkgs := []string{}
|
||||
flakePkgs := []string{}
|
||||
|
||||
packageMap := n.GetPackageMapping(wm)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
// Skip installed packages unless marked for reinstall
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
n.log(fmt.Sprintf("Warning: No package mapping found for %s", dep.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeSystem:
|
||||
nixpkgsPkgs = append(nixpkgsPkgs, pkgInfo.Name)
|
||||
case RepoTypeFlake:
|
||||
flakePkgs = append(flakePkgs, pkgInfo.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nixpkgsPkgs, flakePkgs
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) installNixpkgsPackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
n.log(fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"profile", "install"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing nixpkgs packages...",
|
||||
IsComplete: false,
|
||||
CommandInfo: fmt.Sprintf("nix %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "nix", args...)
|
||||
return n.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) installFlakePackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
n.log(fmt.Sprintf("Installing flake packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
baseProgress := 0.65
|
||||
progressStep := 0.20 / float64(len(packages))
|
||||
|
||||
for i, pkg := range packages {
|
||||
currentProgress := baseProgress + (float64(i) * progressStep)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: currentProgress,
|
||||
Step: fmt.Sprintf("Installing flake package %s (%d/%d)...", pkg, i+1, len(packages)),
|
||||
IsComplete: false,
|
||||
CommandInfo: fmt.Sprintf("nix profile install %s", pkg),
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "nix", "profile", "install", pkg)
|
||||
if err := n.runWithProgress(cmd, progressChan, PhaseAURPackages, currentProgress, currentProgress+progressStep); err != nil {
|
||||
return fmt.Errorf("failed to install flake package %s: %w", pkg, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) postInstallConfig(progressChan chan<- InstallProgressMsg) error {
|
||||
// For NixOS, DMS is installed as a flake package, so we skip both the binary installation and git clone
|
||||
// The flake installation handles both the binary and config files correctly
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.95,
|
||||
Step: "NixOS configuration complete",
|
||||
IsComplete: false,
|
||||
LogOutput: "DMS installed via flake - binary and config handled by Nix",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
605
examples/danklinux/internal/distros/opensuse.go
Normal file
605
examples/danklinux/internal/distros/opensuse.go
Normal file
@@ -0,0 +1,605 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("opensuse-tumbleweed", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewOpenSUSEDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type OpenSUSEDistribution struct {
|
||||
*BaseDistribution
|
||||
*ManualPackageInstaller
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &OpenSUSEDistribution{
|
||||
BaseDistribution: base,
|
||||
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetID() string {
|
||||
return o.config.ID
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetColorHex() string {
|
||||
return o.config.ColorHex
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetFamily() DistroFamily {
|
||||
return o.config.Family
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerZypper
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return o.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
// DMS at the top (shell is prominent)
|
||||
dependencies = append(dependencies, o.detectDMS())
|
||||
|
||||
// Terminal with choice support
|
||||
dependencies = append(dependencies, o.detectSpecificTerminal(terminal))
|
||||
|
||||
// Common detections using base methods
|
||||
dependencies = append(dependencies, o.detectGit())
|
||||
dependencies = append(dependencies, o.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, o.detectQuickshell())
|
||||
dependencies = append(dependencies, o.detectXDGPortal())
|
||||
dependencies = append(dependencies, o.detectPolkitAgent())
|
||||
dependencies = append(dependencies, o.detectAccountsService())
|
||||
|
||||
// Hyprland-specific tools
|
||||
if wm == deps.WindowManagerHyprland {
|
||||
dependencies = append(dependencies, o.detectHyprlandTools()...)
|
||||
}
|
||||
|
||||
// Niri-specific tools
|
||||
if wm == deps.WindowManagerNiri {
|
||||
dependencies = append(dependencies, o.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
// Base detections (common across distros)
|
||||
dependencies = append(dependencies, o.detectMatugen())
|
||||
dependencies = append(dependencies, o.detectDgop())
|
||||
dependencies = append(dependencies, o.detectHyprpicker())
|
||||
dependencies = append(dependencies, o.detectClipboardTools()...)
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if o.packageInstalled("xdg-desktop-portal-gtk") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xdg-desktop-portal-gtk",
|
||||
Status: status,
|
||||
Description: "Desktop integration portal for GTK",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) detectPolkitAgent() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if o.packageInstalled("mate-polkit") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "mate-polkit",
|
||||
Status: status,
|
||||
Description: "PolicyKit authentication agent",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("rpm", "-q", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||
packages := map[string]PackageMapping{
|
||||
// Standard zypper packages
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
|
||||
|
||||
// Manual builds
|
||||
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
||||
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerNiri:
|
||||
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if o.commandExists("xwayland-satellite") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xwayland-satellite",
|
||||
Status: status,
|
||||
Description: "Xwayland support",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if o.packageInstalled("accountsservice") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "accountsservice",
|
||||
Status: status,
|
||||
Description: "D-Bus interface for user account query and manipulation",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
||||
return []string{
|
||||
"make",
|
||||
"unzip",
|
||||
"gcc",
|
||||
"gcc-c++",
|
||||
"cmake",
|
||||
"ninja",
|
||||
"pkgconf-pkg-config",
|
||||
"git",
|
||||
"qt6-base-devel",
|
||||
"qt6-declarative-devel",
|
||||
"qt6-declarative-private-devel",
|
||||
"qt6-shadertools",
|
||||
"qt6-shadertools-devel",
|
||||
"qt6-wayland-devel",
|
||||
"qt6-waylandclient-private-devel",
|
||||
"spirv-tools-devel",
|
||||
"cli11-devel",
|
||||
"wayland-protocols-devel",
|
||||
"libgbm-devel",
|
||||
"libdrm-devel",
|
||||
"pipewire-devel",
|
||||
"jemalloc-devel",
|
||||
"wayland-utils",
|
||||
"Mesa-libGLESv3-devel",
|
||||
"pam-devel",
|
||||
"glib2-devel",
|
||||
"polkit-devel",
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
prerequisites := o.getPrerequisites()
|
||||
var missingPkgs []string
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.06,
|
||||
Step: "Checking prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Checking prerequisite packages",
|
||||
}
|
||||
|
||||
for _, pkg := range prerequisites {
|
||||
checkCmd := exec.CommandContext(ctx, "rpm", "-q", pkg)
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
missingPkgs = append(missingPkgs, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
o.log("go not found in PATH, will install go")
|
||||
missingPkgs = append(missingPkgs, "go")
|
||||
} else {
|
||||
o.log("go already available in PATH")
|
||||
}
|
||||
|
||||
if len(missingPkgs) == 0 {
|
||||
o.log("All prerequisites already installed")
|
||||
return nil
|
||||
}
|
||||
|
||||
o.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.08,
|
||||
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo zypper install -y %s", strings.Join(missingPkgs, " ")),
|
||||
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
|
||||
}
|
||||
|
||||
args := []string{"zypper", "install", "-y"}
|
||||
args = append(args, missingPkgs...)
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
o.logError("failed to install prerequisites", err)
|
||||
o.log(fmt.Sprintf("Prerequisites command output: %s", string(output)))
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
o.log(fmt.Sprintf("Prerequisites install output: %s", string(output)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error {
|
||||
// Phase 1: Check Prerequisites
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
systemPkgs, manualPkgs := o.categorizePackages(dependencies, wm, reinstallFlags)
|
||||
|
||||
// Phase 2: System Packages (Zypper)
|
||||
if len(systemPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||
}
|
||||
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install zypper packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Manual Builds
|
||||
if len(manualPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.85,
|
||||
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||
}
|
||||
if err := o.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: Configuration
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
|
||||
// Phase 5: Complete
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool) ([]string, []string) {
|
||||
systemPkgs := []string{}
|
||||
manualPkgs := []string{}
|
||||
|
||||
variantMap := make(map[string]deps.PackageVariant)
|
||||
for _, dep := range dependencies {
|
||||
variantMap[dep.Name] = dep.Variant
|
||||
}
|
||||
|
||||
packageMap := o.GetPackageMappingWithVariants(wm, variantMap)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
// Skip installed packages unless marked for reinstall
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
o.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeSystem:
|
||||
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||
case RepoTypeManual:
|
||||
manualPkgs = append(manualPkgs, dep.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return systemPkgs, manualPkgs
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"zypper", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
// installQuickshell overrides the base implementation to set openSUSE-specific CFLAGS
|
||||
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
o.log("Installing quickshell from source (with openSUSE-specific build flags)...")
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return fmt.Errorf("HOME environment variable not set")
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Cloning quickshell repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/quickshell-mirror/quickshell.git",
|
||||
}
|
||||
|
||||
var cloneCmd *exec.Cmd
|
||||
if forceQuickshellGit {
|
||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||
} else {
|
||||
// Get latest tag from repository
|
||||
latestTag := o.getLatestQuickshellTag(ctx)
|
||||
if latestTag != "" {
|
||||
o.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag))
|
||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "--branch", latestTag, "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||
} else {
|
||||
o.log("Warning: failed to fetch latest tag, using default branch")
|
||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||
}
|
||||
}
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone quickshell: %w", err)
|
||||
}
|
||||
|
||||
buildDir := tmpDir + "/build"
|
||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create build directory: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.3,
|
||||
Step: "Configuring quickshell build (with openSUSE flags)...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "cmake -B build -S . -G Ninja",
|
||||
}
|
||||
|
||||
// Get optflags from rpm
|
||||
optflagsCmd := exec.CommandContext(ctx, "rpm", "--eval", "%{optflags}")
|
||||
optflagsOutput, err := optflagsCmd.Output()
|
||||
optflags := strings.TrimSpace(string(optflagsOutput))
|
||||
if err != nil || optflags == "" {
|
||||
o.log("Warning: Could not get optflags from rpm, using default -O2 -g")
|
||||
optflags = "-O2 -g"
|
||||
}
|
||||
|
||||
// Set openSUSE-specific CFLAGS
|
||||
customCFLAGS := fmt.Sprintf("%s -I/usr/include/wayland", optflags)
|
||||
|
||||
configureCmd := exec.CommandContext(ctx, "cmake", "-GNinja", "-B", "build",
|
||||
"-DCMAKE_BUILD_TYPE=RelWithDebInfo",
|
||||
"-DCRASH_REPORTER=off",
|
||||
"-DCMAKE_CXX_STANDARD=20")
|
||||
configureCmd.Dir = tmpDir
|
||||
configureCmd.Env = append(os.Environ(),
|
||||
"TMPDIR="+cacheDir,
|
||||
"CFLAGS="+customCFLAGS,
|
||||
"CXXFLAGS="+customCFLAGS)
|
||||
|
||||
o.log(fmt.Sprintf("Using CFLAGS: %s", customCFLAGS))
|
||||
|
||||
output, err := configureCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
o.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
|
||||
return fmt.Errorf("failed to configure quickshell: %w\nCMake output:\n%s", err, string(output))
|
||||
}
|
||||
|
||||
o.log(fmt.Sprintf("cmake configure successful. Output:\n%s", string(output)))
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.4,
|
||||
Step: "Building quickshell (this may take a while)...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "cmake --build build",
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "build")
|
||||
buildCmd.Dir = tmpDir
|
||||
buildCmd.Env = append(os.Environ(),
|
||||
"TMPDIR="+cacheDir,
|
||||
"CFLAGS="+customCFLAGS,
|
||||
"CXXFLAGS="+customCFLAGS)
|
||||
if err := o.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.8, "Building quickshell..."); err != nil {
|
||||
return fmt.Errorf("failed to build quickshell: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.8,
|
||||
Step: "Installing quickshell...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo cmake --install build",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cd %s && cmake --install build", tmpDir))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||
}
|
||||
|
||||
o.log("quickshell installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if o.commandExists("cargo") {
|
||||
return nil
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.82,
|
||||
Step: "Installing rustup...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo zypper install rustup",
|
||||
}
|
||||
|
||||
rustupInstallCmd := execSudoCommand(ctx, sudoPassword, "zypper install -y rustup")
|
||||
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||
return fmt.Errorf("failed to install rustup: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.83,
|
||||
Step: "Installing stable Rust toolchain...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "rustup install stable",
|
||||
}
|
||||
|
||||
rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable")
|
||||
if err := o.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil {
|
||||
return fmt.Errorf("failed to install Rust toolchain: %w", err)
|
||||
}
|
||||
|
||||
if !o.commandExists("cargo") {
|
||||
o.log("Warning: cargo not found in PATH after Rust installation, trying to source environment")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallManualPackages overrides the base implementation to use openSUSE-specific builds
|
||||
func (o *OpenSUSEDistribution) InstallManualPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
o.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
// Install Rust if needed for matugen
|
||||
for _, pkg := range packages {
|
||||
if pkg == "matugen" {
|
||||
if err := o.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Rust: %w", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg == "quickshell" {
|
||||
if err := o.installQuickshell(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Use the base ManualPackageInstaller for other packages
|
||||
if err := o.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
115
examples/danklinux/internal/distros/osinfo.go
Normal file
115
examples/danklinux/internal/distros/osinfo.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/errdefs"
|
||||
)
|
||||
|
||||
// DistroInfo contains basic information about a distribution
|
||||
type DistroInfo struct {
|
||||
ID string
|
||||
HexColorCode string
|
||||
}
|
||||
|
||||
// OSInfo contains complete OS information
|
||||
type OSInfo struct {
|
||||
Distribution DistroInfo
|
||||
Version string
|
||||
VersionID string
|
||||
PrettyName string
|
||||
Architecture string
|
||||
}
|
||||
|
||||
// GetOSInfo detects the current OS and returns information about it
|
||||
func GetOSInfo() (*OSInfo, error) {
|
||||
if runtime.GOOS != "linux" {
|
||||
return nil, errdefs.NewCustomError(errdefs.ErrTypeNotLinux, fmt.Sprintf("Only linux is supported, but I found %s", runtime.GOOS))
|
||||
}
|
||||
|
||||
if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
|
||||
return nil, errdefs.NewCustomError(errdefs.ErrTypeInvalidArchitecture, fmt.Sprintf("Only amd64 and arm64 are supported, but I found %s", runtime.GOARCH))
|
||||
}
|
||||
|
||||
info := &OSInfo{
|
||||
Architecture: runtime.GOARCH,
|
||||
}
|
||||
|
||||
file, err := os.Open("/etc/os-release")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := parts[0]
|
||||
value := strings.Trim(parts[1], "\"")
|
||||
|
||||
switch key {
|
||||
case "ID":
|
||||
config, exists := Registry[value]
|
||||
if !exists {
|
||||
return nil, errdefs.NewCustomError(errdefs.ErrTypeUnsupportedDistribution, fmt.Sprintf("Unsupported distribution: %s", value))
|
||||
}
|
||||
|
||||
info.Distribution = DistroInfo{
|
||||
ID: value, // Use the actual ID from os-release
|
||||
HexColorCode: config.ColorHex,
|
||||
}
|
||||
case "VERSION_ID", "BUILD_ID":
|
||||
info.VersionID = value
|
||||
case "VERSION":
|
||||
info.Version = value
|
||||
case "PRETTY_NAME":
|
||||
info.PrettyName = value
|
||||
}
|
||||
}
|
||||
|
||||
return info, scanner.Err()
|
||||
}
|
||||
|
||||
// IsUnsupportedDistro checks if a distribution/version combination is supported
|
||||
func IsUnsupportedDistro(distroID, versionID string) bool {
|
||||
if !IsDistroSupported(distroID) {
|
||||
return true
|
||||
}
|
||||
|
||||
if distroID == "ubuntu" {
|
||||
parts := strings.Split(versionID, ".")
|
||||
if len(parts) >= 2 {
|
||||
major, err1 := strconv.Atoi(parts[0])
|
||||
minor, err2 := strconv.Atoi(parts[1])
|
||||
|
||||
if err1 == nil && err2 == nil {
|
||||
return major < 25 || (major == 25 && minor < 4)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if distroID == "debian" {
|
||||
if versionID == "" {
|
||||
// debian testing/sid have no version ID
|
||||
return false
|
||||
}
|
||||
versionNum, err := strconv.Atoi(versionID)
|
||||
if err == nil {
|
||||
return versionNum < 12
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
752
examples/danklinux/internal/distros/ubuntu.go
Normal file
752
examples/danklinux/internal/distros/ubuntu.go
Normal file
@@ -0,0 +1,752 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("ubuntu", "#E95420", FamilyUbuntu, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewUbuntuDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type UbuntuDistribution struct {
|
||||
*BaseDistribution
|
||||
*ManualPackageInstaller
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewUbuntuDistribution(config DistroConfig, logChan chan<- string) *UbuntuDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &UbuntuDistribution{
|
||||
BaseDistribution: base,
|
||||
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) GetID() string {
|
||||
return u.config.ID
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) GetColorHex() string {
|
||||
return u.config.ColorHex
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) GetFamily() DistroFamily {
|
||||
return u.config.Family
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerAPT
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return u.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
// DMS at the top (shell is prominent)
|
||||
dependencies = append(dependencies, u.detectDMS())
|
||||
|
||||
// Terminal with choice support
|
||||
dependencies = append(dependencies, u.detectSpecificTerminal(terminal))
|
||||
|
||||
// Common detections using base methods
|
||||
dependencies = append(dependencies, u.detectGit())
|
||||
dependencies = append(dependencies, u.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, u.detectQuickshell())
|
||||
dependencies = append(dependencies, u.detectXDGPortal())
|
||||
dependencies = append(dependencies, u.detectPolkitAgent())
|
||||
dependencies = append(dependencies, u.detectAccountsService())
|
||||
|
||||
// Hyprland-specific tools
|
||||
if wm == deps.WindowManagerHyprland {
|
||||
dependencies = append(dependencies, u.detectHyprlandTools()...)
|
||||
}
|
||||
|
||||
// Niri-specific tools
|
||||
if wm == deps.WindowManagerNiri {
|
||||
dependencies = append(dependencies, u.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
// Base detections (common across distros)
|
||||
dependencies = append(dependencies, u.detectMatugen())
|
||||
dependencies = append(dependencies, u.detectDgop())
|
||||
dependencies = append(dependencies, u.detectHyprpicker())
|
||||
dependencies = append(dependencies, u.detectClipboardTools()...)
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if u.packageInstalled("xdg-desktop-portal-gtk") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xdg-desktop-portal-gtk",
|
||||
Status: status,
|
||||
Description: "Desktop integration portal for GTK",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) detectPolkitAgent() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if u.packageInstalled("mate-polkit") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "mate-polkit",
|
||||
Status: status,
|
||||
Description: "PolicyKit authentication agent",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if u.commandExists("xwayland-satellite") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xwayland-satellite",
|
||||
Status: status,
|
||||
Description: "Xwayland support",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if u.packageInstalled("accountsservice") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "accountsservice",
|
||||
Status: status,
|
||||
Description: "D-Bus interface for user account query and manipulation",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("dpkg", "-l", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
packages := map[string]PackageMapping{
|
||||
// Standard APT packages
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"},
|
||||
|
||||
// Manual builds (niri and quickshell likely not available in Ubuntu repos or PPAs)
|
||||
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
||||
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
|
||||
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
// Use the cppiber PPA for Hyprland
|
||||
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
||||
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerNiri:
|
||||
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.06,
|
||||
Step: "Updating package lists...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Updating APT package lists",
|
||||
}
|
||||
|
||||
updateCmd := execSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||
return fmt.Errorf("failed to update package lists: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.08,
|
||||
Step: "Installing build-essential...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install -y build-essential",
|
||||
LogOutput: "Installing build tools",
|
||||
}
|
||||
|
||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
// Not installed, install it
|
||||
cmd := execSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
||||
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.10,
|
||||
Step: "Installing development dependencies...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev",
|
||||
LogOutput: "Installing additional development tools",
|
||||
}
|
||||
|
||||
devToolsCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
||||
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||
return fmt.Errorf("failed to install development tools: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.12,
|
||||
Step: "Prerequisites installation complete",
|
||||
IsComplete: false,
|
||||
LogOutput: "Prerequisites successfully installed",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error {
|
||||
// Phase 1: Check Prerequisites
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := u.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
systemPkgs, ppaPkgs, manualPkgs := u.categorizePackages(dependencies, wm, reinstallFlags)
|
||||
|
||||
// Phase 2: Enable PPA repositories
|
||||
if len(ppaPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.15,
|
||||
Step: "Enabling PPA repositories...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Setting up PPA repositories for additional packages",
|
||||
}
|
||||
if err := u.enablePPARepos(ctx, ppaPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to enable PPA repositories: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: System Packages (APT)
|
||||
if len(systemPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||
}
|
||||
if err := u.installAPTPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install APT packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: PPA Packages
|
||||
ppaPkgNames := u.extractPackageNames(ppaPkgs)
|
||||
if len(ppaPkgNames) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages, // Reusing AUR phase for PPA
|
||||
Progress: 0.65,
|
||||
Step: fmt.Sprintf("Installing %d PPA packages...", len(ppaPkgNames)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing PPA packages: %s", strings.Join(ppaPkgNames, ", ")),
|
||||
}
|
||||
if err := u.installPPAPackages(ctx, ppaPkgNames, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install PPA packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: Manual Builds
|
||||
if len(manualPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.80,
|
||||
Step: "Installing build dependencies...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Installing build tools for manual compilation",
|
||||
}
|
||||
if err := u.installBuildDependencies(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install build dependencies: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.85,
|
||||
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||
}
|
||||
if err := u.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 6: Configuration
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
|
||||
// Phase 7: Complete
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool) ([]string, []PackageMapping, []string) {
|
||||
systemPkgs := []string{}
|
||||
ppaPkgs := []PackageMapping{}
|
||||
manualPkgs := []string{}
|
||||
|
||||
packageMap := u.GetPackageMapping(wm)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
// Skip installed packages unless marked for reinstall
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
u.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeSystem:
|
||||
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||
case RepoTypePPA:
|
||||
ppaPkgs = append(ppaPkgs, pkgInfo)
|
||||
case RepoTypeManual:
|
||||
manualPkgs = append(manualPkgs, dep.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return systemPkgs, ppaPkgs, manualPkgs
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||
names := make([]string, len(packages))
|
||||
for i, pkg := range packages {
|
||||
names[i] = pkg.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
enabledRepos := make(map[string]bool)
|
||||
|
||||
installPPACmd := execSudoCommand(ctx, sudoPassword,
|
||||
"apt-get install -y software-properties-common")
|
||||
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
||||
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
||||
}
|
||||
|
||||
for _, pkg := range ppaPkgs {
|
||||
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||
u.log(fmt.Sprintf("Enabling PPA repository: %s", pkg.RepoURL))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.20,
|
||||
Step: fmt.Sprintf("Enabling PPA repo %s...", pkg.RepoURL),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
||||
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
||||
return fmt.Errorf("failed to enable PPA repo %s: %w", pkg.RepoURL, err)
|
||||
}
|
||||
u.log(fmt.Sprintf("PPA repo %s enabled successfully", pkg.RepoURL))
|
||||
enabledRepos[pkg.RepoURL] = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(enabledRepos) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.25,
|
||||
Step: "Updating package lists...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get update",
|
||||
}
|
||||
|
||||
updateCmd := execSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.70,
|
||||
Step: "Installing PPA packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
buildDeps := make(map[string]bool)
|
||||
|
||||
for _, pkg := range manualPkgs {
|
||||
switch pkg {
|
||||
case "niri":
|
||||
buildDeps["curl"] = true
|
||||
buildDeps["libxkbcommon-dev"] = true
|
||||
buildDeps["libwayland-dev"] = true
|
||||
buildDeps["libudev-dev"] = true
|
||||
buildDeps["libinput-dev"] = true
|
||||
buildDeps["libdisplay-info-dev"] = true
|
||||
buildDeps["libpango1.0-dev"] = true
|
||||
buildDeps["libcairo-dev"] = true
|
||||
buildDeps["libpipewire-0.3-dev"] = true
|
||||
buildDeps["libc6-dev"] = true
|
||||
buildDeps["clang"] = true
|
||||
buildDeps["libseat-dev"] = true
|
||||
buildDeps["libgbm-dev"] = true
|
||||
buildDeps["alacritty"] = true
|
||||
buildDeps["fuzzel"] = true
|
||||
buildDeps["libxcb-cursor-dev"] = true
|
||||
case "quickshell":
|
||||
buildDeps["qt6-base-dev"] = true
|
||||
buildDeps["qt6-base-private-dev"] = true
|
||||
buildDeps["qt6-declarative-dev"] = true
|
||||
buildDeps["qt6-declarative-private-dev"] = true
|
||||
buildDeps["qt6-wayland-dev"] = true
|
||||
buildDeps["qt6-wayland-private-dev"] = true
|
||||
buildDeps["qt6-tools-dev"] = true
|
||||
buildDeps["libqt6svg6-dev"] = true
|
||||
buildDeps["qt6-shadertools-dev"] = true
|
||||
buildDeps["spirv-tools"] = true
|
||||
buildDeps["libcli11-dev"] = true
|
||||
buildDeps["libjemalloc-dev"] = true
|
||||
buildDeps["libwayland-dev"] = true
|
||||
buildDeps["wayland-protocols"] = true
|
||||
buildDeps["libdrm-dev"] = true
|
||||
buildDeps["libgbm-dev"] = true
|
||||
buildDeps["libegl-dev"] = true
|
||||
buildDeps["libgles2-mesa-dev"] = true
|
||||
buildDeps["libgl1-mesa-dev"] = true
|
||||
buildDeps["libxcb1-dev"] = true
|
||||
buildDeps["libpipewire-0.3-dev"] = true
|
||||
buildDeps["libpam0g-dev"] = true
|
||||
case "ghostty":
|
||||
buildDeps["curl"] = true
|
||||
buildDeps["libgtk-4-dev"] = true
|
||||
buildDeps["libadwaita-1-dev"] = true
|
||||
case "matugen":
|
||||
buildDeps["curl"] = true
|
||||
case "cliphist":
|
||||
// Go will be installed separately with PPA
|
||||
}
|
||||
}
|
||||
|
||||
for _, pkg := range manualPkgs {
|
||||
switch pkg {
|
||||
case "niri", "matugen":
|
||||
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Rust: %w", err)
|
||||
}
|
||||
case "ghostty":
|
||||
if err := u.installZig(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Zig: %w", err)
|
||||
}
|
||||
case "cliphist", "dgop":
|
||||
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Go: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(buildDeps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
depList := make([]string, 0, len(buildDeps))
|
||||
for dep := range buildDeps {
|
||||
depList = append(depList, dep)
|
||||
}
|
||||
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, depList...)
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if u.commandExists("cargo") {
|
||||
return nil
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.82,
|
||||
Step: "Installing rustup...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install rustup",
|
||||
}
|
||||
|
||||
rustupInstallCmd := execSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
||||
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||
return fmt.Errorf("failed to install rustup: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.83,
|
||||
Step: "Installing stable Rust toolchain...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "rustup install stable",
|
||||
}
|
||||
|
||||
rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable")
|
||||
if err := u.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil {
|
||||
return fmt.Errorf("failed to install Rust toolchain: %w", err)
|
||||
}
|
||||
|
||||
// Verify cargo is now available
|
||||
if !u.commandExists("cargo") {
|
||||
u.log("Warning: cargo not found in PATH after Rust installation, trying to source environment")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installZig(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if u.commandExists("zig") {
|
||||
return nil
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
zigUrl := "https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz"
|
||||
zigTmp := filepath.Join(cacheDir, "zig.tar.xz")
|
||||
|
||||
downloadCmd := exec.CommandContext(ctx, "curl", "-L", zigUrl, "-o", zigTmp)
|
||||
if err := u.runWithProgress(downloadCmd, progressChan, PhaseSystemPackages, 0.84, 0.85); err != nil {
|
||||
return fmt.Errorf("failed to download Zig: %w", err)
|
||||
}
|
||||
|
||||
extractCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("tar -xf %s -C /opt/", zigTmp))
|
||||
if err := u.runWithProgress(extractCmd, progressChan, PhaseSystemPackages, 0.85, 0.86); err != nil {
|
||||
return fmt.Errorf("failed to extract Zig: %w", err)
|
||||
}
|
||||
|
||||
linkCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"ln -sf /opt/zig-linux-x86_64-0.11.0/zig /usr/local/bin/zig")
|
||||
return u.runWithProgress(linkCmd, progressChan, PhaseSystemPackages, 0.86, 0.87)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if u.commandExists("go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.87,
|
||||
Step: "Adding Go PPA repository...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
||||
}
|
||||
|
||||
addPPACmd := execSudoCommand(ctx, sudoPassword,
|
||||
"add-apt-repository -y ppa:longsleep/golang-backports")
|
||||
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
||||
return fmt.Errorf("failed to add Go PPA: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.88,
|
||||
Step: "Updating package lists...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get update",
|
||||
}
|
||||
|
||||
updateCmd := execSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
||||
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.89,
|
||||
Step: "Installing Go...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install golang-go",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
||||
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installGhosttyUbuntu(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
u.log("Installing Ghostty using Ubuntu installer script...")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Running Ghostty Ubuntu installer...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
|
||||
LogOutput: "Installing Ghostty using pre-built Ubuntu package",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
|
||||
|
||||
if err := u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
|
||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||
}
|
||||
|
||||
u.log("Ghostty installed successfully using Ubuntu installer")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Override InstallManualPackages for Ubuntu to handle Ubuntu-specific installations
|
||||
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
u.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
for _, pkg := range packages {
|
||||
switch pkg {
|
||||
case "ghostty":
|
||||
if err := u.installGhosttyUbuntu(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||
}
|
||||
default:
|
||||
// Use the base ManualPackageInstaller for other packages
|
||||
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user