477 lines
11 KiB
Go
477 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"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
|
|
}
|
|
|
|
func parseOsRelease(osRelease string) *OS {
|
|
var result OS
|
|
result.ID = "Unknown"
|
|
result.Name = "Unknown"
|
|
result.Version = "Unknown"
|
|
|
|
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], "\"")
|
|
}
|
|
}
|
|
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 getPackageManager(os *OS) (string, error) {
|
|
switch os.ID {
|
|
case "debian", "ubuntu":
|
|
return "apt", nil
|
|
case "arch":
|
|
return "pacman", nil
|
|
case "fedora":
|
|
return "dnf", nil
|
|
default:
|
|
var pmcommands = []string{
|
|
"apt",
|
|
"dnf",
|
|
"pacman",
|
|
}
|
|
for _, pmname := range pmcommands {
|
|
_, err := exec.LookPath(pmname)
|
|
if err == nil {
|
|
return pmname, 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 getInstallCommand(pm string) (string, error) {
|
|
switch pm {
|
|
case "apt":
|
|
return "apt install -y", nil
|
|
case "pacman":
|
|
return "pacman -S --noconfirm", nil
|
|
case "dnf":
|
|
return "dnf install -y", nil
|
|
default:
|
|
return "", fmt.Errorf("no install command found for package manager: %s", pm)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
return fmt.Errorf("failed to install %s: %v\n%s", pkg, err, string(output))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// func installPackage(cmd, pkg string) error {
|
|
// fullCmd := fmt.Sprintf("%s %s", cmd, pkg)
|
|
// command := exec.Command("sudo", "sh", "-c", fullCmd)
|
|
// output, err := command.CombinedOutput()
|
|
// if err != 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 installSpecialSoftware() error {
|
|
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, "", ""); 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 -- -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
|
|
}
|
|
|
|
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) 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,
|
|
}
|
|
}
|
|
|
|
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(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"`
|
|
}
|
|
|
|
var installCommand string
|
|
|
|
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)
|
|
}
|
|
pm, err := getPackageManager(os)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
installCommand, err = getInstallCommand(pm)
|
|
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...)
|
|
}
|
|
|
|
m := newModel(packages, sudoPassword)
|
|
p := tea.NewProgram(m)
|
|
if _, err := p.Run(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
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 := installSpecialSoftware(); err != nil {
|
|
log.Printf("Fehler beim Installieren der speziellen Software: %v", err)
|
|
} else {
|
|
fmt.Println("Spezielle Software erfolgreich installiert")
|
|
}
|
|
} else {
|
|
fmt.Println("Installation der speziellen Pakete übersprungen")
|
|
}
|
|
|
|
if err := installSpecialPackages(cfg.SpecialPackages); err != nil {
|
|
log.Printf("Fehler bei der Installation spezieller Pakete: %v", err)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
if err := rootCmd.Execute(); err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
}
|