diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..658b3b8 --- /dev/null +++ b/cmd.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var rootCmd = &cobra.Command{ + Use: "package-installer", + Short: "Installiert Pakete basierend auf TOML-Konfiguration", + Run: run, +} + +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) + } +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..35ec318 --- /dev/null +++ b/config.go @@ -0,0 +1,9 @@ +package main + +type Config struct { + Headless bool `mapstructure:"headless"` + Packages Packages `mapstructure:"packages"` + SpecialPackages SpecialPackages `mapstructure:"special_packages"` + Flatpak FlatpakConfig `mapstructure:"flatpak"` + Dotfiles DotfilesConfig `mapstructure:"dotfiles"` +} diff --git a/dotfiles.go b/dotfiles.go new file mode 100644 index 0000000..e493afc --- /dev/null +++ b/dotfiles.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" +) + +type DotfilesConfig struct { + Enable bool `mapstructure:"enable"` + GitRepo string `mapstructure:"git_repo"` +} + +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) + } + + 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 +} diff --git a/flatpak.go b/flatpak.go new file mode 100644 index 0000000..8db20ee --- /dev/null +++ b/flatpak.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "log" + "os/exec" + + "github.com/charmbracelet/bubbles/progress" +) + +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"` +} + +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 +} diff --git a/main.go b/main.go index 855bf53..e61393f 100644 --- a/main.go +++ b/main.go @@ -2,801 +2,9 @@ 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) diff --git a/model.go b/model.go new file mode 100644 index 0000000..532537f --- /dev/null +++ b/model.go @@ -0,0 +1,163 @@ +package main + +import ( + "fmt" + "log" + "os/exec" + "runtime" + "strings" + + "github.com/charmbracelet/bubbles/progress" + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type model struct { + packages []string + index int + width int + height int + spinner spinner.Model + progress progress.Model + done bool + sudoPassword string + os OS +} + +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 +} + +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 +} + +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) + } +} diff --git a/osinfo.go b/osinfo.go new file mode 100644 index 0000000..ca2aee5 --- /dev/null +++ b/osinfo.go @@ -0,0 +1,126 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "strings" +) + +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: + 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 (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) + } +} + +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 +} diff --git a/package.go b/package.go new file mode 100644 index 0000000..7ddebc2 --- /dev/null +++ b/package.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "os/exec" + "strings" +) + +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"` +} + +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 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 printUnavailablePackages() { + if len(unavailablePackages) > 0 { + fmt.Println("\nFolgende Pakete waren nicht verfügbar:") + for _, pkg := range unavailablePackages { + fmt.Printf("- %s\n", pkg) + } + } +} diff --git a/specialSoftware.go b/specialSoftware.go new file mode 100644 index 0000000..4c1c7f3 --- /dev/null +++ b/specialSoftware.go @@ -0,0 +1,122 @@ +package main + +import ( + "fmt" + "log" + "os/exec" + "runtime" + + "github.com/charmbracelet/bubbles/progress" + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +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 +} + +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) + } +} diff --git a/tui.go b/tui.go new file mode 100644 index 0000000..f542b21 --- /dev/null +++ b/tui.go @@ -0,0 +1,166 @@ +package main + +import ( + "fmt" + "log" + "os/exec" + + "github.com/charmbracelet/bubbles/progress" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211")) + doneStyle = lipgloss.NewStyle().Margin(1, 2) + checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓") + unavailablePackages []string +) + +type installedItemMsg string + +type installedPkgMsg string + +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) + } + } +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..6bbbb99 --- /dev/null +++ b/utils.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "os" + "runtime" + + "github.com/charmbracelet/huh" +) + +func getSudoPassword() (string, error) { + var password string + form := huh.NewForm( + huh.NewGroup( + huh.NewInput(). + Title("Bitte geben Sie Ihr sudo-Passwort ein"). + EchoMode(huh.EchoModePassword). + Value(&password), + ), + ).WithTheme(huh.ThemeCatppuccin()) + + err := form.Run() + if err != nil { + return "", fmt.Errorf("Fehler bei der Passwortabfrage: %v", err) + } + return password, 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 max(a, b int) int { + if a > b { + return a + } + return b +}