Files
unitdore/cmd/install.go
T
warkanum 91811b4fa1 feat: order/delay in service files, enable cmd, --all on unit commands
- install/uninstall/start/stop/enable take <name> or --all; *all kept as shortcuts
- add enable (enable at boot + start); startall no longer enables
- order/delay resolved by config.ResolveOrder and written into systemd,
  OpenRC and runit service files (After=/sleep), so they apply at boot
- install --all stops and disables units before removing their files
- update regenerates all installed, enabled units and removes disabled ones
- edit creates the config dir; docs and man page updated
- tests: fake Init for cmd, ResolveOrder, backend ordering output
2026-10-12 21:31:41 +02:00

173 lines
4.6 KiB
Go

package cmd
import (
"fmt"
"io"
"os"
"github.com/spf13/cobra"
"github.com/warkanum/unitdore/config"
"github.com/warkanum/unitdore/initsys"
)
var dryRun, installAllFlag bool
var installCmd = &cobra.Command{
Use: "install <name> | --all",
Short: "Generate and install the service file for one unit (or all with --all)",
Long: `Install generates the service file for the named unit using the configured
init system. It does not enable or start it. The unit must be enabled in the
config.
With --all it installs every enabled unit (same as 'installall'); disabled
units that are installed are stopped, disabled and removed.
Use --dry-run to preview the generated files without writing anything.`,
Args: nameOrAll(&installAllFlag),
RunE: runInstall,
}
var installallCmd = &cobra.Command{
Use: "installall",
Short: "Generate and install service files for all enabled units",
Long: `Installall generates service files for all enabled units using the configured
init system. It does not enable or start them. Units that are disabled in the
config are stopped, disabled and their service files removed.
Use --dry-run to preview the generated files without writing anything.`,
RunE: runInstallall,
}
func init() {
installCmd.Flags().BoolVar(&installAllFlag, "all", false, "install all enabled units")
for _, c := range []*cobra.Command{installCmd, installallCmd} {
c.Flags().BoolVar(&dryRun, "dry-run", false, "preview service files without writing")
rootCmd.AddCommand(c)
}
}
func runInstall(cmd *cobra.Command, args []string) error {
cfg, err := config.Load(configPath)
if err != nil {
return err
}
ini, err := initsys.New(cfg)
if err != nil {
return err
}
if installAllFlag {
installAll(cfg, ini, dryRun, os.Stdout)
return nil
}
return installOne(cfg, ini, args[0], dryRun, os.Stdout)
}
func runInstallall(cmd *cobra.Command, args []string) error {
cfg, err := config.Load(configPath)
if err != nil {
return err
}
ini, err := initsys.New(cfg)
if err != nil {
return err
}
installAll(cfg, ini, dryRun, os.Stdout)
return nil
}
// installOne installs a single enabled unit. Disabled units are an error:
// removing files is installall's and update's job.
func installOne(cfg *config.Config, ini initsys.Init, name string, dry bool, w io.Writer) error {
u := cfg.FindUnit(name)
if u == nil {
return fmt.Errorf("unit %q not found", name)
}
if !u.Enabled {
return fmt.Errorf("unit %q is disabled — run 'unitdore update %s --enable' first", name, name)
}
prefix, suffix := cfg.Prefix, cfg.Suffix
path, err := ini.UnitPath(*u, prefix, suffix)
if err != nil {
return err
}
if dry {
content, err := ini.Generate(*u, prefix, suffix)
if err != nil {
return err
}
fmt.Fprintf(w, "\n--- %s ---\n%s\n", path, content)
return nil
}
if err := ini.Install(*u, prefix, suffix); err != nil {
return err
}
fmt.Fprintf(w, " ✓ installed: %s\n", path)
lingerHint(w, ini, map[string]bool{}, *u)
return nil
}
func installAll(cfg *config.Config, ini initsys.Init, dry bool, w io.Writer) {
prefix, suffix := cfg.Prefix, cfg.Suffix
installed, skipped, removed := 0, 0, 0
lingerSeen := map[string]bool{}
for _, u := range cfg.Units {
name := ini.ServiceName(u, prefix, suffix)
if !u.Enabled {
if !ini.IsInstalled(u, prefix, suffix) {
skipped++
continue
}
if dry {
fmt.Fprintf(w, " ~ would remove: %s\n", name)
continue
}
// Stop and disable first so the service doesn't outlive its file
if err := ini.Disable(u, prefix, suffix); err != nil {
fmt.Fprintf(w, " ✗ failed to disable %s: %v\n", u.Name, err)
continue
}
if err := ini.Uninstall(u, prefix, suffix); err != nil {
fmt.Fprintf(w, " ✗ failed to remove %s: %v\n", u.Name, err)
continue
}
fmt.Fprintf(w, " - removed: %s (disabled)\n", name)
removed++
continue
}
if dry {
content, err := ini.Generate(u, prefix, suffix)
if err != nil {
fmt.Fprintf(w, " ✗ %s: %v\n", u.Name, err)
continue
}
path, err := ini.UnitPath(u, prefix, suffix)
if err != nil {
fmt.Fprintf(w, " ✗ %s: %v\n", u.Name, err)
continue
}
fmt.Fprintf(w, "\n--- %s ---\n%s\n", path, content)
installed++
continue
}
if err := ini.Install(u, prefix, suffix); err != nil {
fmt.Fprintf(w, " ✗ failed: %s: %v\n", u.Name, err)
continue
}
path, _ := ini.UnitPath(u, prefix, suffix)
fmt.Fprintf(w, " ✓ installed: %s\n", path)
lingerHint(w, ini, lingerSeen, u)
installed++
}
verb := "Installed"
if dry {
verb = "Would install"
}
fmt.Fprintf(w, "\nDone. %s: %d Removed: %d Skipped: %d\n", verb, installed, removed, skipped)
}