system_setup_tool/main.go

805 lines
20 KiB
Go

package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/charmbracelet/bubbles/progress"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type OS struct {
ID string
Name string
Version string
PackageManager string
InstallCommand string
}
func parseOsRelease(osRelease string) *OS {
var result OS
result.ID = "Unknown"
result.Name = "Unknown"
result.Version = "Unknown"
result.PackageManager = "Unkown"
result.InstallCommand = "Unkown"
lines := strings.Split(osRelease, "\n")
for _, line := range lines {
splitLine := strings.SplitN(line, "=", 2)
if len(splitLine) != 2 {
continue
}
switch splitLine[0] {
case "ID":
result.ID = strings.ToLower(strings.Trim(splitLine[1], "\""))
case "NAME":
result.Name = strings.Trim(splitLine[1], "\"")
case "VERSION_ID":
result.Version = strings.Trim(splitLine[1], "\"")
}
}
err := result.getPackageManager()
if err != nil {
log.Fatal(err)
}
err = result.getInstallCommand()
if err != nil {
log.Fatal(err)
}
return &result
}
func getLinuxDistribution() (*OS, error) {
_, err := os.Stat("/etc/os-release")
if os.IsNotExist(err) {
return nil, fmt.Errorf("unable to read system information")
}
osRelease, _ := os.ReadFile("/etc/os-release")
return parseOsRelease(string(osRelease)), nil
}
func (os *OS) getPackageManager() error {
switch os.ID {
case "debian", "ubuntu":
os.PackageManager = "apt"
return nil
case "arch":
os.PackageManager = "pacman"
return nil
case "fedora":
os.PackageManager = "dnf"
return nil
default:
var pmcommands = []string{
"apt",
"dnf",
"pacman",
}
for _, pmname := range pmcommands {
_, err := exec.LookPath(pmname)
if err == nil {
os.PackageManager = pmname
return nil
}
}
return fmt.Errorf("no packagemanager found for os: %s", os)
}
}
func getSudoPassword() (string, error) {
var password string
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Bitte geben Sie Ihr sudo-Passwort ein").
Password(true).
Value(&password),
),
).WithTheme(huh.ThemeCatppuccin())
err := form.Run()
if err != nil {
return "", fmt.Errorf("Fehler bei der Passwortabfrage: %v", err)
}
return password, nil
}
func installBuildEssentials(os *OS, sudoPassword string) error {
var command string
switch os.PackageManager {
case "pacman":
command = "pacman -S --noconfirm --needed base-devel"
case "apt":
command = "apt install -y build-essential"
case "dnf":
command = "dnf install -y @development-tools"
default:
return fmt.Errorf("keine Build Essentials für OS %s definiert", os.ID)
}
fmt.Printf("Installiere Build Essentials für %s...\n", os.Name)
if err := installPackage(command, "", sudoPassword); err != nil {
return fmt.Errorf("Fehler bei der Installation der Build Essentials: %v", err)
}
return nil
}
func (os *OS) getInstallCommand() error {
switch os.PackageManager {
case "apt":
os.InstallCommand = "apt install -y"
return nil
case "pacman":
os.InstallCommand = "pacman -S --noconfirm --needed"
return nil
case "dnf":
os.InstallCommand = "dnf install -y --best"
return nil
default:
return fmt.Errorf("no install command found for package manager: %s", os.ID)
}
}
type specialSoftwareModel struct {
items []string
index int
spinner spinner.Model
progress progress.Model
done bool
sudoPassword string
}
func newSpecialSoftwareModel(sudoPassword string) specialSoftwareModel {
items := []string{"oh-my-posh", "golang", "rust"}
p := progress.New(
progress.WithDefaultGradient(),
progress.WithWidth(40),
progress.WithoutPercentage(),
)
s := spinner.New()
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
return specialSoftwareModel{
items: items,
spinner: s,
progress: p,
sudoPassword: sudoPassword,
}
}
func (m specialSoftwareModel) Init() tea.Cmd {
return tea.Batch(m.installItemCmd(m.items[m.index]), m.spinner.Tick)
}
func (m specialSoftwareModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
return m, tea.Quit
}
case installedItemMsg:
if m.index >= len(m.items)-1 {
m.done = true
return m, tea.Quit
}
m.index++
progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.items)))
return m, tea.Batch(
progressCmd,
tea.Printf("%s %s", checkMark, m.items[m.index-1]),
m.installItemCmd(m.items[m.index]),
)
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}
return m, nil
}
func (m specialSoftwareModel) View() string {
if m.done {
return doneStyle.Render(fmt.Sprintf("Spezielle Software Installation abgeschlossen!\n"))
}
spin := m.spinner.View() + " "
prog := m.progress.View()
info := fmt.Sprintf("Installiere %s", m.items[m.index])
return spin + info + " " + prog
}
type installedItemMsg string
func (m specialSoftwareModel) installItemCmd(item string) tea.Cmd {
return func() tea.Msg {
switch item {
case "oh-my-posh":
if _, err := exec.LookPath("oh-my-posh"); err == nil {
return installedItemMsg(item)
}
poshCommand := "curl -s https://ohmyposh.dev/install.sh | bash -s"
if err := installPackage(poshCommand, "", ""); err != nil {
log.Printf("Fehler beim Installieren von oh-my-posh: %v", err)
}
case "golang":
if _, err := exec.LookPath("go"); err == nil {
return installedItemMsg(item)
}
golangVersion := "1.23.4"
if err := downloadGolang(golangVersion); err != nil {
log.Printf("Fehler beim Herunterladen von Go: %v", err)
}
golangCommand := fmt.Sprintf("sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go%s.linux-%s.tar.gz", golangVersion, runtime.GOARCH)
if err := installPackage(golangCommand, "", m.sudoPassword); err != nil {
log.Printf("Fehler beim Installieren von Go: %v", err)
}
case "rust":
if _, err := exec.LookPath("rustc"); err == nil {
return installedItemMsg(item)
}
rustupCommand := "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -q -y"
if err := installPackage(rustupCommand, "", ""); err != nil {
log.Printf("Fehler beim Installieren von Rust: %v", err)
}
}
return installedItemMsg(item)
}
}
var unavailablePackages []string
func installPackage(cmd, pkg, sudoPassword string) error {
fullCmd := fmt.Sprintf("%s %s", cmd, pkg)
command := exec.Command("sudo", "-S", "sh", "-c", fullCmd)
command.Stdin = strings.NewReader(sudoPassword + "\n")
output, err := command.CombinedOutput()
if err != nil {
if strings.Contains(string(output), "not found") || strings.Contains(string(output), "no matching package") || strings.Contains(string(output), "Keine Übereinstimmung") || strings.Contains(string(output), "Ziel nicht gefunden") {
unavailablePackages = append(unavailablePackages, pkg)
return nil
}
return fmt.Errorf("failed to install %s: %v\n%s", pkg, err, string(output))
}
return nil
}
func downloadGolang(golangVersion string) error {
var link string
if runtime.GOARCH == "arm64" {
link = "https://go.dev/dl/go" + golangVersion + ".linux-arm64.tar.gz"
} else {
link = "https://go.dev/dl/go" + golangVersion + ".linux-amd64.tar.gz"
}
resp, err := http.Get(link)
if err != nil {
return fmt.Errorf("Fehler beim Herunterladen von Go: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Unerwarteter HTTP-Status: %s", resp.Status)
}
outFile, err := os.Create("go" + golangVersion + ".linux-" + runtime.GOARCH + ".tar.gz")
if err != nil {
return fmt.Errorf("Fehler beim Erstellen der Ausgabedatei: %v", err)
}
defer outFile.Close()
_, err = io.Copy(outFile, resp.Body)
if err != nil {
return fmt.Errorf("Fehler beim Schreiben der Ausgabedatei: %v", err)
}
return nil
}
func (m model) installSpecialSoftware() error {
if _, err := exec.LookPath("oh-my-posh"); err == nil {
fmt.Println("Oh-my-posh ist bereits installiert")
} else {
poshCommand := "curl -s https://ohmyposh.dev/install.sh | bash -s"
if err := installPackage(poshCommand, "", ""); err != nil {
return err
}
}
if _, err := exec.LookPath("go"); err == nil {
fmt.Println("Go ist bereits installiert.")
} else {
golangVersion := "1.23.4"
if err := downloadGolang(golangVersion); err != nil {
return fmt.Errorf("Fehler beim Herunterladen von Go: %v", err)
}
golangCommand := fmt.Sprintf("sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go%s.linux-%s.tar.gz", golangVersion, runtime.GOARCH)
if err := installPackage(golangCommand, "", m.sudoPassword); err != nil {
return err
}
}
if _, err := exec.LookPath("rustc"); err == nil {
fmt.Println("Rust ist bereits installiert.")
} else {
rustupCommand := "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -q -y"
if err := installPackage(rustupCommand, "", ""); err != nil {
return err
}
}
// if _, err := exec.LookPath("pipx"); err == nil {
// fmt.Println("Pipx ist bereits installiert.")
// } else {
// pipXCommand := "python3 -m pip install --user pipx && python3 -m pipx ensurepath"
// if err := installPackage(pipXCommand, "", ""); err != nil {
// return err
// }
// }
return nil
}
type model struct {
packages []string
index int
width int
height int
spinner spinner.Model
progress progress.Model
done bool
sudoPassword string
os OS
}
var (
currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211"))
doneStyle = lipgloss.NewStyle().Margin(1, 2)
checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓")
)
func newModel(packages []string, sudoPassword string, os *OS) model {
p := progress.New(
progress.WithDefaultGradient(),
progress.WithWidth(40),
progress.WithoutPercentage(),
)
s := spinner.New()
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
return model{
packages: packages,
spinner: s,
progress: p,
sudoPassword: sudoPassword,
os: *os,
}
}
func (m model) Init() tea.Cmd {
return tea.Batch(m.installPackageCmd(m.packages[m.index]), m.spinner.Tick)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
return m, tea.Quit
}
case installedPkgMsg:
pkg := m.packages[m.index]
if m.index >= len(m.packages)-1 {
m.done = true
return m, tea.Sequence(
tea.Printf("%s %s", checkMark, pkg),
tea.Quit,
)
}
m.index++
progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.packages)))
return m, tea.Batch(
progressCmd,
tea.Printf("%s %s", checkMark, pkg),
m.installPackageCmd(m.packages[m.index]),
)
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
case progress.FrameMsg:
newModel, cmd := m.progress.Update(msg)
if newModel, ok := newModel.(progress.Model); ok {
m.progress = newModel
}
return m, cmd
}
return m, nil
}
func (m model) View() string {
n := len(m.packages)
w := lipgloss.Width(fmt.Sprintf("%d", n))
if m.done {
return doneStyle.Render(fmt.Sprintf("Done! Installed %d packages.\n", n))
}
pkgCount := fmt.Sprintf(" %*d/%*d", w, m.index, w, n)
spin := m.spinner.View() + " "
prog := m.progress.View()
cellsAvail := max(0, m.width-lipgloss.Width(spin+prog+pkgCount))
pkgName := currentPkgNameStyle.Render(m.packages[m.index])
info := lipgloss.NewStyle().MaxWidth(cellsAvail).Render("Installing " + pkgName)
cellsRemaining := max(0, m.width-lipgloss.Width(spin+info+prog+pkgCount))
gap := strings.Repeat(" ", cellsRemaining)
return spin + info + gap + prog + pkgCount
}
type installedPkgMsg string
func (m model) installPackageCmd(pkg string) tea.Cmd {
return func() tea.Msg {
if err := installPackage(m.os.InstallCommand, pkg, m.sudoPassword); err != nil {
log.Printf("Fehler beim Installieren von %s: %v", pkg, err)
}
return installedPkgMsg(pkg)
}
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
var rootCmd = &cobra.Command{
Use: "package-installer",
Short: "Installiert Pakete basierend auf TOML-Konfiguration",
Run: run,
}
type Packages struct {
Headless []string `mapstructure:"headless"`
NonHeadless []string `mapstructure:"non_headless"`
}
type SpecialPackages struct {
Go []string `mapstructure:"go"`
Cargo []string `mapstructure:"cargo"`
Pipx []string `mapstructure:"pipx"`
}
type Config struct {
Headless bool `mapstructure:"headless"`
Packages Packages `mapstructure:"packages"`
SpecialPackages SpecialPackages `mapstructure:"special_packages"`
Flatpak FlatpakConfig `mapstructure:"flatpak"`
Dotfiles DotfilesConfig `mapstructure:"dotfiles"`
}
type FlatpakConfig struct {
Enable bool `mapstructure:"enable"`
Remotes []Remote `mapstructure:"remotes"`
Packages []string `mapstructure:"packages"`
}
type Remote struct {
Name string `mapstructure:"name"`
URL string `mapstructure:"url"`
}
type DotfilesConfig struct {
Enable bool `mapstructure:"enable"`
GitRepo string `mapstructure:"git_repo"`
}
func installFlatpak(os *OS, sudoPassword string) error {
var command string
switch os.PackageManager {
case "pacman":
command = "pacman -S --noconfirm --needed flatpak"
case "apt":
command = "apt install -y flatpak"
case "dnf":
command = "dnf install -y flatpak"
default:
return fmt.Errorf("keine Flatpak-Installation für OS %s definiert", os.ID)
}
if err := installPackage(command, "", sudoPassword); err != nil {
return fmt.Errorf("Fehler bei der Flatpak-Installation: %v", err)
}
return nil
}
func addFlatpakRemotes(remotes []Remote) error {
for _, remote := range remotes {
cmd := exec.Command("flatpak", "remote-add", "--if-not-exists", remote.Name, remote.URL)
if err := cmd.Run(); err != nil {
return fmt.Errorf("Fehler beim Hinzufügen des Remotes %s: %v", remote.Name, err)
}
}
return nil
}
func installFlatpakPackages(packages []string) error {
if len(packages) == 0 {
return nil
}
fmt.Println("\nInstalliere Flatpak-Pakete...")
p := progress.New(
progress.WithDefaultGradient(),
progress.WithWidth(40),
)
for i, pkg := range packages {
p.SetPercent(float64(i) / float64(len(packages)))
fmt.Printf("Installiere Flatpak: %s\n", pkg)
cmd := exec.Command("flatpak", "install", "-y", pkg)
if err := cmd.Run(); err != nil {
log.Printf("Fehler bei der Installation von %s: %v", pkg, err)
continue
}
}
return nil
}
func setupDotfiles(config DotfilesConfig) error {
if _, err := exec.LookPath("git"); err != nil {
return fmt.Errorf("git ist nicht installiert")
}
if _, err := exec.LookPath("stow"); err != nil {
return fmt.Errorf("gnu stow ist nicht installiert")
}
dotfilesDir := filepath.Join(os.Getenv("HOME"), "dotfiles")
cmd := exec.Command("git", "clone", config.GitRepo)
if err := cmd.Run(); err != nil {
return fmt.Errorf("Fehler beim Klonen der Dotfiles: %v", err)
}
if err := os.Chdir(dotfilesDir); err != nil {
return fmt.Errorf("Fehler beim Wechseln in das Dotfiles-Verzeichnis: %v", err)
}
// Module mit stow verlinken
cmd = exec.Command("stow", ".")
if err := cmd.Run(); err != nil {
log.Printf("Fehler beim Linken: %v", err)
}
fmt.Printf("Alles erfolgreich verlinkt\n")
return nil
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringP("config", "c", "", "Pfad zur Konfigurationsdatei")
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
}
func initConfig() {
if cfgFile := viper.GetString("config"); cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
viper.SetConfigName("config")
viper.AddConfigPath(".")
}
if err := viper.ReadInConfig(); err != nil {
fmt.Println("Fehler beim Lesen der Konfigurationsdatei:", err)
os.Exit(1)
}
}
func installSpecialPackages(sp SpecialPackages) error {
for _, pkg := range sp.Go {
cmd := exec.Command("go", "install", pkg+"@latest")
if err := cmd.Run(); err != nil {
return fmt.Errorf("Fehler bei der Installation von %s: %v", pkg, err)
}
}
for _, pkg := range sp.Cargo {
cmd := exec.Command("cargo", "install", pkg)
if err := cmd.Run(); err != nil {
return fmt.Errorf("Fehler bei der Installation von %s: %v", pkg, err)
}
}
for _, pkg := range sp.Pipx {
cmd := exec.Command("pipx", "install", pkg)
if err := cmd.Run(); err != nil {
return fmt.Errorf("Fehler bei der Installation von %s: %v", pkg, err)
}
}
return nil
}
func run(cmd *cobra.Command, args []string) {
os, err := getLinuxDistribution()
if err != nil {
log.Fatal(err)
}
sudoPassword, err := getSudoPassword()
if err != nil {
log.Fatal(err)
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
log.Fatalf("Fehler beim Lesen der Konfiguration: %v", err)
}
form := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("Möchten Sie eine headless Installation durchführen?").
Value(&cfg.Headless),
),
).WithTheme(huh.ThemeCatppuccin())
if err := form.Run(); err != nil {
log.Fatalf("Fehler bei der Benutzerabfrage: %v", err)
}
var packages []string
packages = append(packages, cfg.Packages.Headless...)
if !cfg.Headless {
packages = append(packages, cfg.Packages.NonHeadless...)
}
unavailablePackages = []string{}
m := newModel(packages, sudoPassword, os)
p := tea.NewProgram(m)
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
printUnavailablePackages()
var installSpecial bool
form = huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("Möchten Sie die speziellen Pakete (Go, Rust, Pipx) installieren?").
Value(&installSpecial),
),
).WithTheme(huh.ThemeCatppuccin())
err = form.Run()
if err != nil {
log.Fatal(err)
}
if installSpecial {
if err := installBuildEssentials(os, sudoPassword); err != nil {
log.Printf("Warnung: %v", err)
}
sm := newSpecialSoftwareModel(sudoPassword)
p = tea.NewProgram(sm)
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
if len(cfg.SpecialPackages.Go) > 0 || len(cfg.SpecialPackages.Cargo) > 0 || len(cfg.SpecialPackages.Pipx) > 0 {
var allSpecialPkgs []struct {
typ string
name string
command string
}
for _, pkg := range cfg.SpecialPackages.Go {
allSpecialPkgs = append(allSpecialPkgs, struct {
typ string
name string
command string
}{"go", pkg, "go install " + pkg + "@latest"})
}
for _, pkg := range cfg.SpecialPackages.Cargo {
allSpecialPkgs = append(allSpecialPkgs, struct {
typ string
name string
command string
}{"cargo", pkg, "cargo install " + pkg})
}
for _, pkg := range cfg.SpecialPackages.Pipx {
allSpecialPkgs = append(allSpecialPkgs, struct {
typ string
name string
command string
}{"pipx", pkg, "pipx install " + pkg})
}
if len(allSpecialPkgs) > 0 {
fmt.Println("\nInstalliere spezielle Pakete...")
p := progress.New(
progress.WithDefaultGradient(),
progress.WithWidth(40),
)
for i, pkg := range allSpecialPkgs {
p.SetPercent(float64(i) / float64(len(allSpecialPkgs)))
fmt.Printf("Installiere %s Paket: %s\n", pkg.typ, pkg.name)
cmd := exec.Command("sh", "-c", pkg.command)
if err := cmd.Run(); err != nil {
log.Printf("Fehler bei der Installation von %s: %v", pkg.name, err)
continue
}
}
}
}
if cfg.Flatpak.Enable {
fmt.Println("\nKonfiguriere Flatpak...")
if err := installFlatpak(os, sudoPassword); err != nil {
log.Printf("Warnung bei Flatpak-Installation: %v", err)
}
if err := addFlatpakRemotes(cfg.Flatpak.Remotes); err != nil {
log.Printf("Warnung bei Flatpak-Remotes: %v", err)
}
if err := installFlatpakPackages(cfg.Flatpak.Packages); err != nil {
log.Printf("Warnung bei Flatpak-Paketen: %v", err)
}
}
if cfg.Dotfiles.Enable {
fmt.Println("\nKonfiguriere Dotfiles...")
if err := setupDotfiles(cfg.Dotfiles); err != nil {
log.Printf("Warnung bei Dotfiles-Setup: %v", err)
}
}
}
func printUnavailablePackages() {
if len(unavailablePackages) > 0 {
fmt.Println("\nFolgende Pakete waren nicht verfügbar:")
for _, pkg := range unavailablePackages {
fmt.Printf("- %s\n", pkg)
}
}
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}