diff --git a/core/cmd/dms/commands_greeter.go b/core/cmd/dms/commands_greeter.go index 720ea29ad..86ffdb16d 100644 --- a/core/cmd/dms/commands_greeter.go +++ b/core/cmd/dms/commands_greeter.go @@ -1081,8 +1081,11 @@ func ensureGreetdEnabled() error { if !runitServiceInstalled("greetd") { return fmt.Errorf("greetd service not found in %s. Please install greetd first", runitSvDir) } - // Seat + runtime-dir setup that logind handles automatically on systemd. - ensureRunitSeat("_greeter") + if greeter.IsVoidLinux() { + ensureVoidLogindGreeter("_greeter") + } else { + ensureRunitSeat("_greeter") + } ensureGreetdPamRundir() if err := enableRunitService("greetd"); err != nil { return fmt.Errorf("failed to enable greetd: %w", err) @@ -1261,6 +1264,9 @@ func enableGreeter(nonInteractive bool) error { if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil { fmt.Printf("⚠ Could not ensure cache directory: %v\n Run: sudo mkdir -p %s && sudo chown root:%s %s && sudo chmod 2770 %s\n", err, greeter.GreeterCacheDir, greeterGroup, greeter.GreeterCacheDir, greeter.GreeterCacheDir) } + if err := greeter.EnsureVoidLogindGreetdCommand(logFunc, ""); err != nil { + return err + } if err := ensureGraphicalTarget(); err != nil { return err @@ -1475,12 +1481,23 @@ func extractGreeterWrapperFromCommand(command string) string { if len(tokens) == 0 { return "" } - wrapper := strings.Trim(tokens[0], "\"") + wrapperIndex := 0 + if filepath.Base(strings.Trim(tokens[0], "\"")) == "env" { + wrapperIndex = 1 + for wrapperIndex < len(tokens) && strings.Contains(tokens[wrapperIndex], "=") { + wrapperIndex++ + } + } + if wrapperIndex >= len(tokens) { + return "" + } + + wrapper := strings.Trim(tokens[wrapperIndex], "\"") if wrapper == "" { return "" } - if len(tokens) > 1 { - next := strings.Trim(tokens[1], "\"") + if wrapperIndex+1 < len(tokens) { + next := strings.Trim(tokens[wrapperIndex+1], "\"") if next != "" && (filepath.Base(wrapper) == "bash" || filepath.Base(wrapper) == "sh") && strings.Contains(filepath.Base(next), "dms-greeter") { return fmt.Sprintf("%s (script: %s)", wrapper, next) } diff --git a/core/cmd/dms/commands_greeter_test.go b/core/cmd/dms/commands_greeter_test.go index 11489612c..dd087cad5 100644 --- a/core/cmd/dms/commands_greeter_test.go +++ b/core/cmd/dms/commands_greeter_test.go @@ -101,6 +101,13 @@ func TestGreeterStatusStateDirHonorsExplicitOverrideOnNixOS(t *testing.T) { } } +func TestExtractGreeterWrapperFromEnvCommand(t *testing.T) { + command := "env LIBSEAT_BACKEND=logind DMS_VOID=1 /usr/bin/dms-greeter --command niri" + if got := extractGreeterWrapperFromCommand(command); got != "/usr/bin/dms-greeter" { + t.Fatalf("extractGreeterWrapperFromCommand() = %q, want %q", got, "/usr/bin/dms-greeter") + } +} + func TestRejectNixOSGreeterMutationBlocksImperativeCommands(t *testing.T) { origGreeterIsNixOSFn := greeterIsNixOSFn greeterIsNixOSFn = func() bool { return true } diff --git a/core/cmd/dms/init_service.go b/core/cmd/dms/init_service.go index f28519cd0..d1e92a9dc 100644 --- a/core/cmd/dms/init_service.go +++ b/core/cmd/dms/init_service.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" "github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" ) @@ -83,9 +84,31 @@ func ensureRunitSeat(greeterUser string) { } } -// ensureGreetdPamRundir adds pam_rundir to the greetd PAM stack so the post-login -// session gets an XDG_RUNTIME_DIR on systems without logind (Void with seatd). -// Appended outside DMS's managed auth block so it survives `dms greeter sync`. +// ensureVoidLogindGreeter configures the elogind-backed greeter on Void. +func ensureVoidLogindGreeter(greeterUser string) { + for _, service := range []string{"dbus", "elogind"} { + if err := enableRunitService(service); err != nil { + fmt.Printf(" ⚠ could not enable %s: %v\n", service, err) + } else { + fmt.Printf(" ✓ %s enabled\n", service) + } + } + greeter.EnsureVoidGreetdRunScript(func(msg string) { fmt.Println(" " + msg) }, "") + if runitServiceEnabled("seatd") { + if err := disableRunitService("seatd"); err != nil { + fmt.Printf(" ⚠ could not disable seatd: %v\n", err) + } else { + fmt.Println(" ✓ seatd disabled (elogind manages the seat)") + } + } + if err := privesc.Run(context.Background(), "", "usermod", "-aG", "video,input", greeterUser); err != nil { + fmt.Printf(" ⚠ could not add %s to video/input groups: %v\n", greeterUser, err) + } else { + fmt.Printf(" ✓ %s added to video/input groups (elogind manages the seat)\n", greeterUser) + } +} + +// ensureGreetdPamRundir provides XDG_RUNTIME_DIR to runit greeter sessions. func ensureGreetdPamRundir() { const pamPath = "/etc/pam.d/greetd" data, err := os.ReadFile(pamPath) diff --git a/core/internal/distros/void.go b/core/internal/distros/void.go index f83f907ef..c3b10923a 100644 --- a/core/internal/distros/void.go +++ b/core/internal/distros/void.go @@ -72,6 +72,7 @@ func (v *VoidDistribution) DetectDependenciesWithTerminal(ctx context.Context, w dependencies = append(dependencies, v.detectAccountsService()) dependencies = append(dependencies, v.detectDBus()) dependencies = append(dependencies, v.detectElogind()) + dependencies = append(dependencies, v.detectMesaDri()) if wm == deps.WindowManagerHyprland { dependencies = append(dependencies, v.detectHyprlandTools()...) @@ -142,6 +143,10 @@ func (v *VoidDistribution) detectElogind() deps.Dependency { return v.detectPackage("elogind", "loginctl/logind provider for power management and session tracking", v.packageInstalled("elogind") || v.commandExists("loginctl")) } +func (v *VoidDistribution) detectMesaDri() deps.Dependency { + return v.detectPackage("mesa-dri", "Mesa DRI/EGL drivers (GPU rendering; compositors find no outputs without it)", v.packageInstalled("mesa-dri")) +} + func (v *VoidDistribution) detectXwaylandSatellite() deps.Dependency { return v.detectPackage("xwayland-satellite", "Xwayland support", v.packageInstalled("xwayland-satellite")) } @@ -172,6 +177,7 @@ func (v *VoidDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, "dbus": {Name: "dbus", Repository: RepoTypeSystem}, "elogind": {Name: "elogind", Repository: RepoTypeSystem}, + "mesa-dri": {Name: "mesa-dri", Repository: RepoTypeSystem}, "quickshell": {Name: "quickshell", Repository: RepoTypeSystem}, "matugen": {Name: "matugen", Repository: RepoTypeSystem}, @@ -305,6 +311,7 @@ func (v *VoidDistribution) ensureSessionServices(ctx context.Context, sudoPasswo return nil } + // D-Bus activation alone starts elogind without its wrapper mounts; the runit service is required. for _, service := range []string{"dbus", "elogind"} { if !v.runitServiceInstalled(service) { v.log(fmt.Sprintf("Warning: %s runit service not found in %s; power/session actions may not work until %s is installed", service, voidRunitSvDir, service)) diff --git a/core/internal/greeter/installer.go b/core/internal/greeter/installer.go index af81ad3e7..d48546f11 100644 --- a/core/internal/greeter/installer.go +++ b/core/internal/greeter/installer.go @@ -115,6 +115,52 @@ func ensureRunitSeat(greeterUser, sudoPassword string, logFunc func(string)) { } } +// VoidGreetdRunScript orders greetd after dbus/elogind so the first greeter session can't race elogind's runtime-dir setup. +const VoidGreetdRunScript = `#!/bin/sh +sv check dbus >/dev/null || exit 1 +sv check elogind >/dev/null || exit 1 +exec greetd +` + +// EnsureVoidGreetdRunScript rewrites /etc/sv/greetd/run with dbus/elogind ordering (greetd updates restore stock; enable re-asserts). +func EnsureVoidGreetdRunScript(logFunc func(string), sudoPassword string) { + const runPath = "/etc/sv/greetd/run" + if data, err := os.ReadFile(runPath); err == nil && strings.Contains(string(data), "sv check elogind") { + logFunc("✓ greetd run script already waits for elogind") + return + } + script := fmt.Sprintf("cat > %s <<'EOF'\n%sEOF\nchmod 755 %s", runPath, VoidGreetdRunScript, runPath) + if err := privesc.Run(context.Background(), sudoPassword, "sh", "-c", script); err != nil { + logFunc(fmt.Sprintf("⚠ could not update %s: %v", runPath, err)) + return + } + logFunc("✓ greetd run script now waits for dbus/elogind") +} + +// ensureVoidLogindGreeter configures the elogind-backed greeter on Void. +func ensureVoidLogindGreeter(greeterUser, sudoPassword string, logFunc func(string)) { + for _, service := range []string{"dbus", "elogind"} { + if err := enableRunitService(service, sudoPassword); err != nil { + logFunc(fmt.Sprintf("⚠ could not enable %s: %v", service, err)) + } else { + logFunc(fmt.Sprintf("✓ %s enabled", service)) + } + } + EnsureVoidGreetdRunScript(logFunc, sudoPassword) + if runitServiceEnabled("seatd") { + if err := disableRunitService("seatd", sudoPassword); err != nil { + logFunc(fmt.Sprintf("⚠ could not disable seatd: %v", err)) + } else { + logFunc("✓ seatd disabled (elogind manages the seat)") + } + } + if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", "video,input", greeterUser); err != nil { + logFunc(fmt.Sprintf("⚠ could not add %s to video/input groups: %v", greeterUser, err)) + } else { + logFunc(fmt.Sprintf("✓ %s added to video/input groups (elogind manages the seat)", greeterUser)) + } +} + func ensureGreetdPamRundir(sudoPassword string, logFunc func(string)) { const pamPath = "/etc/pam.d/greetd" data, err := os.ReadFile(pamPath) @@ -1740,6 +1786,10 @@ func syncGreeterColorSource(homeDir, cacheDir string, state greeterThemeSyncStat } func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { + if err := EnsureVoidLogindGreetdCommand(logFunc, sudoPassword); err != nil { + return err + } + homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get user home directory: %w", err) @@ -2271,13 +2321,7 @@ vt = 1 return fmt.Errorf("failed to read greetd config: %w", err) } - wrapperCmd := resolveGreeterWrapperPath() - - compositorLower := strings.ToLower(compositor) - commandValue := fmt.Sprintf("%s --command %s --cache-dir %s", wrapperCmd, compositorLower, GreeterCacheDir) - if dmsPath != "" { - commandValue = fmt.Sprintf("%s -p %s", commandValue, dmsPath) - } + commandValue := buildGreetdCommand(resolveGreeterWrapperPath(), compositor, dmsPath, IsVoidLinux()) commandLine := fmt.Sprintf(`command = "%s"`, commandValue) newConfig := upsertDefaultSession(configContent, greeterUser, commandLine) @@ -2289,6 +2333,84 @@ vt = 1 return nil } +func buildGreetdCommand(wrapperCmd, compositor, dmsPath string, useVoidLogind bool) string { + commandValue := fmt.Sprintf("%s --command %s --cache-dir %s", wrapperCmd, strings.ToLower(compositor), GreeterCacheDir) + if dmsPath != "" { + commandValue = fmt.Sprintf("%s -p %s", commandValue, dmsPath) + } + if useVoidLogind { + commandValue = "env LIBSEAT_BACKEND=logind DMS_VOID=1 " + commandValue + } + return commandValue +} + +// EnsureVoidLogindGreetdCommand migrates DMS greeter commands on Void. +func EnsureVoidLogindGreetdCommand(logFunc func(string), sudoPassword string) error { + if !IsVoidLinux() { + return nil + } + + const configPath = "/etc/greetd/config.toml" + data, err := os.ReadFile(configPath) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return fmt.Errorf("failed to read greetd config: %w", err) + } + + configContent := string(data) + command := extractDefaultSessionCommand(configContent) + if command == "" || !strings.Contains(command, "dms-greeter") { + return nil + } + + migratedCommand := voidLogindGreeterCommand(command) + if migratedCommand == command { + return nil + } + + greeterUser := extractDefaultSessionUser(configContent) + if greeterUser == "" { + greeterUser = DetectGreeterUser() + } + newConfig := upsertDefaultSession(configContent, greeterUser, fmt.Sprintf(`command = "%s"`, migratedCommand)) + return writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, "✓ Updated existing Void greeter to use elogind") +} + +func extractDefaultSessionCommand(configContent string) string { + inDefaultSession := false + for line := range strings.SplitSeq(configContent, "\n") { + if section, ok := parseTomlSection(line); ok { + inDefaultSession = section == "default_session" + continue + } + if !inDefaultSession { + continue + } + + trimmed := stripTomlComment(line) + if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") { + continue + } + parts := strings.SplitN(trimmed, "=", 2) + if len(parts) != 2 { + continue + } + if command := strings.Trim(strings.TrimSpace(parts[1]), `"`); command != "" { + return command + } + } + return "" +} + +func voidLogindGreeterCommand(command string) string { + if strings.Contains(command, "LIBSEAT_BACKEND=logind") && strings.Contains(command, "DMS_VOID=1") { + return command + } + return "env LIBSEAT_BACKEND=logind DMS_VOID=1 " + command +} + func stripConfigFlag(command string) string { for _, flag := range []string{" -C ", " --config "} { idx := strings.Index(command, flag) @@ -2430,7 +2552,11 @@ func EnableGreetd(sudoPassword string, logFunc func(string)) error { if !runitServiceInstalled("greetd") { return fmt.Errorf("greetd service not found in %s; ensure greetd is installed", runitSvDir) } - ensureRunitSeat(DetectGreeterUser(), sudoPassword, logFunc) + if IsVoidLinux() { + ensureVoidLogindGreeter(DetectGreeterUser(), sudoPassword, logFunc) + } else { + ensureRunitSeat(DetectGreeterUser(), sudoPassword, logFunc) + } ensureGreetdPamRundir(sudoPassword, logFunc) if err := enableRunitService("greetd", sudoPassword); err != nil { return fmt.Errorf("failed to enable greetd: %w", err) diff --git a/core/internal/greeter/installer_test.go b/core/internal/greeter/installer_test.go index e248a38a2..8f02f226d 100644 --- a/core/internal/greeter/installer_test.go +++ b/core/internal/greeter/installer_test.go @@ -169,6 +169,56 @@ func TestStripDesktopExecCodes(t *testing.T) { } } +func TestBuildGreetdCommand(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + wrapper string + compositor string + dmsPath string + useVoidLogind bool + want string + }{ + { + name: "standard command", + wrapper: "/usr/bin/dms-greeter", + compositor: "Niri", + want: "/usr/bin/dms-greeter --command niri --cache-dir /var/cache/dms-greeter", + }, + { + name: "void selects elogind and keeps custom DMS path", + wrapper: "/usr/bin/dms-greeter", + compositor: "Niri", + dmsPath: "/usr/share/quickshell/dms-greeter", + useVoidLogind: true, + want: "env LIBSEAT_BACKEND=logind DMS_VOID=1 /usr/bin/dms-greeter --command niri --cache-dir /var/cache/dms-greeter -p /usr/share/quickshell/dms-greeter", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := buildGreetdCommand(tt.wrapper, tt.compositor, tt.dmsPath, tt.useVoidLogind); got != tt.want { + t.Fatalf("buildGreetdCommand() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestVoidLogindGreeterCommand(t *testing.T) { + t.Parallel() + + const oldCommand = "/usr/bin/dms-greeter --command niri -C /etc/greetd/niri.kdl" + const want = "env LIBSEAT_BACKEND=logind DMS_VOID=1 " + oldCommand + if got := voidLogindGreeterCommand(oldCommand); got != want { + t.Fatalf("voidLogindGreeterCommand() = %q, want %q", got, want) + } + if got := voidLogindGreeterCommand(want); got != want { + t.Fatalf("voidLogindGreeterCommand() must be idempotent, got %q", got) + } +} + func TestResolveGreeterAutoLoginState(t *testing.T) { t.Parallel() diff --git a/distro/void/README.md b/distro/void/README.md index 8e058dc53..5d2f434a9 100644 --- a/distro/void/README.md +++ b/distro/void/README.md @@ -58,7 +58,8 @@ checkout at `srcpkgs//template` to build or submit it. ## Dependencies Installing `dms` automatically pulls in `quickshell`, `accountsservice`, `dgop`, -`matugen` (which drives the Material You theming), `dbus`, and `elogind`. +`matugen` (which drives the Material You theming), `dbus`, `elogind`, and +`mesa-dri` (GL drivers, required for compositors to render). The rest are optional, install whichever features you want: | Package | Enables | @@ -123,7 +124,7 @@ sudo ln -sf /etc/sv/dbus /var/service/dbus sudo ln -sf /etc/sv/elogind /var/service/elogind ``` -The `dankinstall` Void path does this automatically after installing packages. +The `dankinstall` Void path enables both services after installing packages. ## Greeter (optional) @@ -134,8 +135,16 @@ dms greeter enable # configures greetd + the Void seat/PAM bits below dms greeter sync # optional: share theming with the shell ``` -`dms greeter enable` handles what logind does automatically on systemd: it points -greetd at the greeter, enables `seatd`, adds `_greeter` to the `_seatd`/`video`/ -`input` groups, and adds `pam_rundir` to `/etc/pam.d/greetd` (so the post-login -session gets an `XDG_RUNTIME_DIR`). A Wayland compositor and a working DRM device -(`/dev/dri/card*`) are required and not pulled in automatically. +`dms-greeter` requires D-Bus and elogind. `dms greeter enable` enables the +`dbus` and `elogind` runit services, configures greetd for elogind +(`LIBSEAT_BACKEND=logind`), adds `_greeter` to the `video` and `input` groups, +and adds `pam_rundir` to `/etc/pam.d/greetd` (so the post-login session gets an +`XDG_RUNTIME_DIR`). It disables seatd if enabled: on Void, seatd is the +alternative to elogind, and running both fights over the seat. Greeter sessions +are launched through `dbus-run-session`. A Wayland compositor and a working DRM +device (`/dev/dri/card*`) are required and not pulled in automatically. + +`dms greeter enable` also rewrites `/etc/sv/greetd/run` to wait for the `dbus` +and `elogind` services, preventing a first-boot race that can leave the greeter +on a black screen. greetd package updates restore the stock run script; re-run +`dms greeter enable` afterwards. diff --git a/distro/void/srcpkgs/dms-greeter/template b/distro/void/srcpkgs/dms-greeter/template index ce5e3ceb5..5e59bf741 100644 --- a/distro/void/srcpkgs/dms-greeter/template +++ b/distro/void/srcpkgs/dms-greeter/template @@ -6,7 +6,7 @@ # Setup is done by `dms greeter enable`, not by this package — see distro/void/README.md. pkgname=dms-greeter version=1.5.0 -revision=1 +revision=2 short_desc="DankMaterialShell greeter for greetd" maintainer="AvengeMedia " license="MIT" @@ -14,7 +14,7 @@ homepage="https://danklinux.com" distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${version}.tar.gz" checksum=14f2678d6a15223ba069d1b8c8a21a5564b735d190c231f62ed44fd8bf48677c -depends="greetd quickshell acl-progs seatd pam_rundir" +depends="greetd quickshell acl-progs dbus elogind pam_rundir mesa-dri" # Cache dir the greeter uses as $HOME (owned by greetd's _greeter user). make_dirs="/var/cache/dms-greeter 0750 _greeter _greeter" diff --git a/distro/void/srcpkgs/dms/template b/distro/void/srcpkgs/dms/template index 1169998cf..e6a2fa341 100644 --- a/distro/void/srcpkgs/dms/template +++ b/distro/void/srcpkgs/dms/template @@ -7,7 +7,7 @@ # CI rewrites version/checksum from the selected release tag before building. pkgname=dms version=1.5.0 -revision=1 +revision=2 build_style=go build_wrksrc="core" go_import_path="github.com/AvengeMedia/DankMaterialShell/core" @@ -23,7 +23,7 @@ distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v$ checksum=14f2678d6a15223ba069d1b8c8a21a5564b735d190c231f62ed44fd8bf48677c # Optional feature deps are listed in distro/void/README.md. -depends="quickshell accountsservice dgop matugen dbus elogind" +depends="quickshell accountsservice dgop matugen dbus elogind mesa-dri" post_install() { # QML shell tree (build_style=go already installed the dms binary) diff --git a/quickshell/Modules/Greetd/GreeterContent.qml b/quickshell/Modules/Greetd/GreeterContent.qml index b5b583a87..849796246 100644 --- a/quickshell/Modules/Greetd/GreeterContent.qml +++ b/quickshell/Modules/Greetd/GreeterContent.qml @@ -2001,6 +2001,8 @@ Item { greeterAutoLoginPendingProcess.running = true; pendingLaunchCommand = sessionCmd; pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"]; + if (Quickshell.env("DMS_VOID") === "1") + pendingLaunchEnv.push("LIBSEAT_BACKEND=logind"); memoryFlushTimer.restart(); } @@ -2052,7 +2054,14 @@ Item { const launchEnv = pendingLaunchEnv; pendingLaunchCommand = ""; pendingLaunchEnv = []; - Greetd.launch(sessionCommand.split(" "), launchEnv); + const sessionArgs = sessionCommand.trim().split(/\s+/); + const needsVoidDbusSession = Quickshell.env("DMS_VOID") === "1" + && !Quickshell.env("DBUS_SESSION_BUS_ADDRESS") + && sessionArgs[0] !== "dbus-run-session"; + const launchArgs = needsVoidDbusSession + ? ["dbus-run-session"].concat(sessionArgs) + : sessionArgs; + Greetd.launch(launchArgs, launchEnv); } }