package main import ( "bufio" "fmt" "io" "io/fs" "math" "os" "time" "errors" "os/exec" "strings" "github.com/goccy/go-yaml" "github.com/jwalton/gchalk" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/pflag" "github.com/thoas/go-funk" ) type PackageManager struct { Name string } func (pm *PackageManager) List() (packages []string, err error) { switch pm.Name { case "pacman": out, err := exec.Command("pacman", "-Sql").Output() if err != nil { return nil, err } return strings.Split(string(out), "\n"), nil case "apt-get": out, err := exec.Command("apt-cache", "pkgnames", "-g").Output() if err != nil { return nil, err } return strings.Split(string(out), "\n"), nil case "tdnf", "dnf", "yum": out, err := exec.Command(pm.Name, "list", "available").Output() if err != nil { return nil, err } return funk.Map(strings.Split(string(out), "\n"), func(p string) string { return strings.SplitN(p, " ", 1)[0] }).([]string), nil case "brew": // please no out, err := exec.Command("brew", "formulae").Output() if err != nil { return nil, err } return strings.Split(string(out), "\n"), nil default: return nil, errors.New("Unknown package manager " + pm.Name) } } func (pm *PackageManager) ListInstalled() (packages []string, err error) { switch pm.Name { case "pacman": out, err := exec.Command("pacman", "-Qq").Output() if err != nil { return nil, err } return strings.Split(string(out), "\n"), nil case "apt-get": out, err := exec.Command("dpkg-query", "-f", "${Package}\n", "-W").Output() if err != nil { return nil, err } return strings.Split(string(out), "\n"), nil case "tdnf", "dnf", "yum": out, err := exec.Command(pm.Name, "list", "installed").Output() if err != nil { return nil, err } return funk.Map(strings.Split(string(out), "\n"), func(p string) string { return strings.SplitN(p, " ", 1)[0] }).([]string), nil case "brew": // please no out, err := exec.Command("brew", "list", "--formula").Output() if err != nil { return nil, err } return strings.Split(string(out), "\n"), nil default: return nil, errors.New("Unknown package manager " + pm.Name) } } func (pm *PackageManager) Install(pkg string) error { switch pm.Name { case "pacman": return executeCommandWithFormatting("pacman", "-S", "--noconfirm", pkg) case "apt-get": return executeCommandWithFormatting("apt-get", "install", "-y", pkg) case "tdnf", "dnf", "yum": return executeCommandWithFormatting(pm.Name, "install", "-y", pkg) case "brew": // please no return executeCommandWithFormatting("brew", "install", pkg) default: return errors.New("Unknown package manager " + pm.Name) } } func (pm *PackageManager) Remove(pkg string) error { switch pm.Name { case "pacman": exec.Command("pacman", "-R", pkg).Run() case "apt-get": err := exec.Command("apt-get", "remove", pkg).Run() if err != nil { return err } case "tdnf", "dnf", "yum": err := exec.Command(pm.Name, "remove", pkg).Run() if err != nil { return err } case "brew": // please no err := exec.Command("brew", "remove", pkg).Run() if err != nil { return err } default: return errors.New("Unknown package manager " + pm.Name) } return nil } func executeCommandWithFormatting(command string, args ...string) error { cmd := exec.Command(command, args...) cmd.Env = append(cmd.Env, "LC_ALL=C.UTF-8") stdout, err := cmd.StdoutPipe() checkError(err) stderr, err := cmd.StderrPipe() checkError(err) stdoutReader := bufio.NewReader(stdout) stderrReader := bufio.NewReader(stderr) ch := make(chan uint8) go func() { for { line, err := stdoutReader.ReadString('\n') if err == io.EOF { break } else { checkError(err) } delim := gchalk.Gray("│") + gchalk.Blue(" |") fmt.Println(delim, strings.TrimSpace(line)) } ch <- 0 }() go func() { for { line, err := stderrReader.ReadString('\n') if err == io.EOF { break } else { checkError(err) } delim := gchalk.Gray("│") + gchalk.Red(" |") fmt.Println(delim, strings.TrimSpace(line)) } ch <- 0 }() err = cmd.Start() if err != nil { return err } <-ch <-ch return cmd.Wait() } func commandExists(cmd string) bool { command := exec.Command(cmd) err := command.Start() return err == nil } func detectPackageManager() (manager string) { managers := []string{"pacman", "apt-get", "tdnf", "dnf", "yum", "brew"} for _, manager := range managers { if commandExists(manager) { return manager } } return "" } func terminateWithError(err error) { log.Fatal().Err(err).Msg("") } func checkUserError(err error) { if err != nil { terminateWithError(err) } } func checkError(err error) { if err != nil { log.Panic().Err(err).Msg("") } } func executeRunlevel(runlevel string, tasks []string, shell string) { fmt.Printf("# Running runlevel '%s'\n", runlevel) for _, task := range tasks { fmt.Println(gchalk.Gray("│ # Running task: " + task)) checkUserError(executeCommandWithFormatting(shell, "-c", task)) } } func main() { start := time.Now() log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) var configData []byte if cfg := *pflag.StringP("config", "c", "", "Provide a path to an alternative config file"); cfg == "" { rawConfig, err := os.ReadFile("polymer.yml") if err == fs.ErrNotExist { rawConfig, err = os.ReadFile("polymer.yaml") if err == fs.ErrNotExist { log.Fatal().Msg("Neither polymer.yml or polymer.yaml were found. Aborting.") } else if err != nil { log.Panic().Err(err).Msg("") } } else if err != nil { log.Panic().Err(err).Msg("") } configData = rawConfig } else { rawConfig, err := os.ReadFile(cfg) if err == fs.ErrNotExist { log.Fatal().Msg("Config file " + cfg + " was not found. Aborting.") } else if err != nil { log.Panic().Err(err).Msg("") } configData = rawConfig } var tasks Config yaml.Unmarshal(configData, &tasks) if tasks.Shell == "" { tasks.Shell = "/bin/sh" } pmName := "" if len(tasks.Packages) > 0 { pmName = detectPackageManager() if pmName == "" { log.Fatal().Msg("No supported package manager (pacman, apt-get, yum, dnf, tdnf, brew) was found!") } } executeRunlevel("init", tasks.Execute.Init, tasks.Shell) pm := PackageManager{Name: pmName} fmt.Printf("\n# Installing packages: '%s'\n", strings.Join(tasks.Packages, ", ")) fmt.Println(gchalk.Gray("│ # Invoking " + pmName)) checkUserError(pm.Install(strings.Join(tasks.Packages, ""))) fmt.Printf( gchalk.Green("# Finished with 0 errors in %g seconds!\n"), math.Round(time.Since(start).Seconds()*1000)/1000, ) } type Services struct { Stop []string `yaml:"stop"` Start []string `yaml:"start"` Enable []string `yaml:"enable"` Disable []string `yaml:"disable"` EnableStart []string `yaml:"startup"` DisableStop []string `yaml:"disableAndStop"` } type Runlevels struct { Init []string `yaml:"init"` WithPackages []string `yaml:"withPackages"` WithServices []string `yaml:"withServices"` Final []string `yaml:"final"` } type Config struct { Shell string `yaml:"shell"` Packages []string `yaml:"packages"` Systemd Services `yaml:"systemd"` OpenRC Services `yaml:"openrc"` Firewall []string `yaml:"firewall"` IPtables []string `yaml:"iptables"` Execute Runlevels `yaml:"runlevels"` }