From 465cf7355b26610ad74ff68195a488cdf7b7843d Mon Sep 17 00:00:00 2001 From: purian23 Date: Sun, 21 Jun 2026 01:21:39 -0400 Subject: [PATCH] distros(void linux): initial packaging support --- .gitignore | 6 + core/cmd/dms/commands_greeter.go | 58 +++++++++- core/cmd/dms/init_service.go | 114 +++++++++++++++++++ distro/void/README.md | 90 +++++++++++++++ distro/void/srcpkgs/dms-greeter/INSTALL.msg | 15 +++ distro/void/srcpkgs/dms-greeter/template | 35 ++++++ distro/void/srcpkgs/dms/template | 48 ++++++++ quickshell/Modules/Greetd/assets/dms-greeter | 9 ++ 8 files changed, 371 insertions(+), 4 deletions(-) create mode 100644 core/cmd/dms/init_service.go create mode 100644 distro/void/README.md create mode 100644 distro/void/srcpkgs/dms-greeter/INSTALL.msg create mode 100644 distro/void/srcpkgs/dms-greeter/template create mode 100644 distro/void/srcpkgs/dms/template diff --git a/.gitignore b/.gitignore index 6723df62..34e1249f 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,9 @@ quickshell/dms-plugins __pycache__ .vscode/ + +# Void (xbps) build artifacts +*.xbps +distro/void/temp/ +distro/void/hostdir/ +distro/void/masterdir*/ diff --git a/core/cmd/dms/commands_greeter.go b/core/cmd/dms/commands_greeter.go index f7eae2a7..96fa07d5 100644 --- a/core/cmd/dms/commands_greeter.go +++ b/core/cmd/dms/commands_greeter.go @@ -299,7 +299,7 @@ func installGreeter(nonInteractive bool) error { fmt.Println("\n=== Installation Complete ===") fmt.Println("\nTo start the greeter now, run:") - fmt.Println(" sudo systemctl start greetd") + fmt.Println(startGreeterHint()) fmt.Println("\nOr reboot to see the greeter at next boot.") return nil @@ -326,7 +326,13 @@ func uninstallGreeter(nonInteractive bool) error { } fmt.Println("\nDisabling greetd...") - if err := privesc.Run(context.Background(), "", "systemctl", "disable", "greetd"); err != nil { + if isRunit() { + if err := disableRunitService("greetd"); err != nil { + fmt.Printf(" ⚠ Could not disable greetd: %v\n", err) + } else { + fmt.Println(" ✓ greetd disabled") + } + } else if err := privesc.Run(context.Background(), "", "systemctl", "disable", "greetd"); err != nil { fmt.Printf(" ⚠ Could not disable greetd: %v\n", err) } else { fmt.Println(" ✓ greetd disabled") @@ -449,6 +455,14 @@ func suggestDisplayManagerRestore(nonInteractive bool) { enableDM := func(dm string) { fmt.Printf(" Enabling %s...\n", dm) + if isRunit() { + if err := enableRunitService(dm); err != nil { + fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err) + } else { + fmt.Printf(" ✓ %s enabled (linked into %s).\n", dm, runitServiceDir) + } + return + } if err := privesc.Run(context.Background(), "", "systemctl", "enable", "--force", dm); err != nil { fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err) } else { @@ -495,6 +509,9 @@ func suggestDisplayManagerRestore(nonInteractive bool) { } func isSystemdUnitInstalled(unit string) bool { + if isRunit() { + return runitServiceInstalled(unit) + } cmd := exec.Command("systemctl", "list-unit-files", unit+".service", "--no-legend", "--no-pager") out, err := cmd.Output() return err == nil && strings.Contains(string(out), unit) @@ -943,6 +960,18 @@ func resolveLocalDMSPath() (string, error) { } func disableDisplayManager(dmName string) (bool, error) { + if isRunit() { + if !runitServiceEnabled(dmName) { + return false, nil + } + fmt.Printf("\nDisabling %s (runit)...\n", dmName) + if err := disableRunitService(dmName); err != nil { + return false, fmt.Errorf("failed to disable %s: %w", dmName, err) + } + fmt.Printf(" ✓ %s disabled (removed from %s)\n", dmName, runitServiceDir) + return true, nil + } + state, err := getSystemdServiceState(dmName) if err != nil { return false, fmt.Errorf("failed to check %s state: %w", dmName, err) @@ -996,6 +1025,21 @@ func disableDisplayManager(dmName string) (bool, error) { } func ensureGreetdEnabled() error { + if isRunit() { + fmt.Println("\nEnabling greetd service (runit)...") + 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") + ensureGreetdPamRundir() + if err := enableRunitService("greetd"); err != nil { + return fmt.Errorf("failed to enable greetd: %w", err) + } + fmt.Printf(" ✓ greetd enabled (%s)\n", runitServiceDir) + return nil + } + fmt.Println("\nChecking greetd service status...") state, err := getSystemdServiceState("greetd") @@ -1043,6 +1087,12 @@ func ensureGreetdEnabled() error { } func ensureGraphicalTarget() error { + if isRunit() { + // runit has no targets; a supervised greetd service is the graphical + // login, so there is nothing to set here. + return nil + } + getDefaultCmd := exec.Command("systemctl", "get-default") currentTarget, err := getDefaultCmd.Output() if err != nil { @@ -1176,7 +1226,7 @@ func enableGreeter(nonInteractive bool) error { fmt.Println("\n=== Enable Complete ===") fmt.Println("\nGreeter configuration verified and system state corrected.") fmt.Println("To start the greeter now, run:") - fmt.Println(" sudo systemctl start greetd") + fmt.Println(startGreeterHint()) fmt.Println("\nOr reboot to see the greeter at boot time.") return nil @@ -1257,7 +1307,7 @@ func enableGreeter(nonInteractive bool) error { fmt.Println("\n=== Enable Complete ===") fmt.Println("\nTo start the greeter now, run:") - fmt.Println(" sudo systemctl start greetd") + fmt.Println(startGreeterHint()) fmt.Println("\nOr reboot to see the greeter at boot time.") return nil diff --git a/core/cmd/dms/init_service.go b/core/cmd/dms/init_service.go new file mode 100644 index 00000000..f28519cd --- /dev/null +++ b/core/cmd/dms/init_service.go @@ -0,0 +1,114 @@ +package main + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/privesc" +) + +// runit (Void Linux) service helpers. Services live in /etc/sv and are "enabled" +// by symlinking them into the /var/service supervision dir, so the greeter +// commands branch on isRunit() instead of shelling systemctl. + +const ( + runitSvDir = "/etc/sv" + runitServiceDir = "/var/service" +) + +// isRunit reports whether this system is supervised by runit (Void Linux). +func isRunit() bool { + if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() { + return true + } + if _, err := os.Stat("/run/systemd/system"); err == nil { + return false + } + if fi, err := os.Stat(runitServiceDir); err == nil && fi.IsDir() { + return true + } + return false +} + +func runitServiceInstalled(name string) bool { + fi, err := os.Stat(runitSvDir + "/" + name) + return err == nil && fi.IsDir() +} + +func runitServiceEnabled(name string) bool { + _, err := os.Lstat(runitServiceDir + "/" + name) + return err == nil +} + +// enableRunitService links a service into /var/service (idempotent). +func enableRunitService(name string) error { + if !runitServiceInstalled(name) { + return fmt.Errorf("runit service %q not found in %s", name, runitSvDir) + } + if runitServiceEnabled(name) { + return nil + } + return privesc.Run(context.Background(), "", "ln", "-sf", + runitSvDir+"/"+name, runitServiceDir+"/"+name) +} + +// disableRunitService removes a service's supervision symlink. +func disableRunitService(name string) error { + if !runitServiceEnabled(name) { + return nil + } + return privesc.Run(context.Background(), "", "rm", "-f", + runitServiceDir+"/"+name) +} + +// ensureRunitSeat sets up the seat access a Wayland greeter needs on runit (the +// equivalent of logind on systemd): enables seatd and adds the greeter user to +// the seat/video/input groups. Failures are reported but non-fatal. +func ensureRunitSeat(greeterUser string) { + if runitServiceInstalled("seatd") { + if err := enableRunitService("seatd"); err != nil { + fmt.Printf(" ⚠ could not enable seatd: %v\n", err) + } else { + fmt.Println(" ✓ seatd enabled") + } + } else { + fmt.Println(" ⚠ seatd not installed — the greeter compositor needs it for GPU/seat access") + } + if err := privesc.Run(context.Background(), "", "usermod", "-aG", "_seatd,video,input", greeterUser); err != nil { + fmt.Printf(" ⚠ could not add %s to seat groups: %v\n", greeterUser, err) + } else { + fmt.Printf(" ✓ %s added to seat groups (_seatd, video, input)\n", greeterUser) + } +} + +// 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`. +func ensureGreetdPamRundir() { + const pamPath = "/etc/pam.d/greetd" + data, err := os.ReadFile(pamPath) + if err != nil { + fmt.Printf(" ⚠ could not read %s: %v\n", pamPath, err) + return + } + if strings.Contains(string(data), "pam_rundir") { + return + } + line := "session optional pam_rundir.so" + if err := privesc.Run(context.Background(), "", "sh", "-c", + fmt.Sprintf("printf '%%s\\n' %q >> %s", line, pamPath)); err != nil { + fmt.Printf(" ⚠ could not add pam_rundir to %s: %v\n", pamPath, err) + return + } + fmt.Println(" ✓ pam_rundir added to greetd PAM (provides XDG_RUNTIME_DIR for the session)") +} + +// startGreeterHint returns the init-appropriate "start greetd now" command. +func startGreeterHint() string { + if isRunit() { + return " sudo sv up greetd" + } + return " sudo systemctl start greetd" +} diff --git a/distro/void/README.md b/distro/void/README.md new file mode 100644 index 00000000..2e04dab1 --- /dev/null +++ b/distro/void/README.md @@ -0,0 +1,90 @@ +# Void Linux packaging + +XBPS templates for DankMaterialShell on [Void Linux](https://voidlinux.org). + +| Package | Source repo | Template | +| --- | --- | --- | +| `dms` | DankMaterialShell | [`srcpkgs/dms/template`](srcpkgs/dms/template) | +| `dms-greeter` (optional) | DankMaterialShell | [`srcpkgs/dms-greeter/template`](srcpkgs/dms-greeter/template) | +| `dgop` | AvengeMedia/dgop | maintained in the **danklinux** repo (`distro/void/srcpkgs/dgop`) | +| `danksearch` | AvengeMedia/danksearch | maintained in the **danklinux** repo (`distro/void/srcpkgs/danksearch`) | + +All build from source. + +## Distribution + +These packages target the official +[`void-linux/void-packages`](https://github.com/void-linux/void-packages) +repository, so they install with a plain `xbps-install dms` and no extra setup. +Most dependencies (`quickshell`, `matugen`, `cava`, `niri`, `greetd`, …) are +already in Void; `dgop` and `danksearch` are packaged alongside in the +[danklinux repo](https://github.com/AvengeMedia/danklinux/tree/master/distro/void). + +The templates here are the source of truth: copy each into a void-packages +checkout at `srcpkgs//template` to build or submit it. Only tagged releases +are packaged (no `-git`/nightly variant). + +## Dependencies + +Installing `dms` automatically pulls in `quickshell`, `accountsservice`, `dgop`, +and `matugen` (which drives the Material You theming). The rest are optional — +install whichever features you want: + +| Package | Enables | +| --- | --- | +| `danksearch` | launcher / filesystem search | +| `cava` | audio visualiser widget | +| `qt6-multimedia` | system sound feedback | +| `qt6ct` | Qt app theming | +| `wtype` | virtual keyboard input | +| `power-profiles-daemon` | power profile control | +| `cups-pk-helper` | printer management | +| `NetworkManager` | network control | +| `i2c-tools` | external-monitor brightness (DDC) | +| `niri` / `hyprland` / `sway` | a Wayland compositor (niri is the team's choice) | + +## Building & testing + +Inside a `void-packages` checkout (symlink or copy these `srcpkgs/` dirs in): + +```sh +# build the dependency packages first (dms requires dgop) +./xbps-src pkg dgop +./xbps-src pkg danksearch +./xbps-src pkg dms +./xbps-src pkg dms-greeter # optional + +# lint (xlint ships in the xtools package) +xlint srcpkgs/dms/template + +# install the built packages +sudo xbps-install --repository=hostdir/binpkgs dms dgop +``` + +`dms` requires Go ≥ 1.26 in the build environment (per `core/go.mod`). + +## Running the shell + +DMS is a user-level Wayland shell with **no system service** — start it from your +compositor's autostart, e.g. niri: + +```kdl +spawn-at-startup "dms" "run" +``` + +or Hyprland: `exec-once = dms run`. + +## Greeter (optional) + +Install `dms-greeter`, then let the CLI do the setup: + +```sh +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. diff --git a/distro/void/srcpkgs/dms-greeter/INSTALL.msg b/distro/void/srcpkgs/dms-greeter/INSTALL.msg new file mode 100644 index 00000000..692357ae --- /dev/null +++ b/distro/void/srcpkgs/dms-greeter/INSTALL.msg @@ -0,0 +1,15 @@ +dms-greeter installed. + +Configure and enable it with: + + dms greeter enable + +This points greetd at the greeter and sets up everything Void needs that logind +would handle on systemd: enables seatd, adds the greeter user to the seat/video/ +input groups, and adds pam_rundir to the greetd PAM stack. Optionally sync your +shell theme into the greeter with: + + dms greeter sync + +Requirements not pulled in automatically: a Wayland compositor (niri, hyprland, +sway, …) and a working DRM device (/dev/dri/card*; in a VM, enable virtio-gpu). diff --git a/distro/void/srcpkgs/dms-greeter/template b/distro/void/srcpkgs/dms-greeter/template new file mode 100644 index 00000000..2b16eddb --- /dev/null +++ b/distro/void/srcpkgs/dms-greeter/template @@ -0,0 +1,35 @@ +# Template file for 'dms-greeter' +# +# greetd greeter for DankMaterialShell +# Builds from the same DMS release tarball as 'dms'; keep version/checksum in sync. +# Setup is done by `dms greeter enable`, not by this package — see distro/void/README.md. +pkgname=dms-greeter +version=1.4.6 +revision=1 +short_desc="DankMaterialShell greeter for greetd" +maintainer="AvengeMedia " +license="MIT" +homepage="https://danklinux.com" +distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${version}.tar.gz" +checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61 + +depends="greetd quickshell acl-progs seatd pam_rundir" + +# Cache dir the greeter uses as $HOME (owned by greetd's _greeter user). +make_dirs="/var/cache/dms-greeter 0750 _greeter _greeter" + +do_install() { + # Launcher wrapper -> /usr/bin/dms-greeter + vbin quickshell/Modules/Greetd/assets/dms-greeter + + # Same QML tree as the shell; greeter mode is selected at runtime via DMS_RUN_GREETER. + vmkdir usr/share/quickshell/dms-greeter + vcopy "quickshell/*" usr/share/quickshell/dms-greeter + + # Sample compositor configs for reference + vinstall quickshell/Modules/Greetd/assets/dms-niri.kdl 644 usr/share/dms-greeter + vinstall quickshell/Modules/Greetd/assets/dms-hypr.conf 644 usr/share/dms-greeter + + vdoc quickshell/Modules/Greetd/README.md + vlicense LICENSE +} diff --git a/distro/void/srcpkgs/dms/template b/distro/void/srcpkgs/dms/template new file mode 100644 index 00000000..131ac2e1 --- /dev/null +++ b/distro/void/srcpkgs/dms/template @@ -0,0 +1,48 @@ +# Template file for 'dms' +# +# DankMaterialShell stable release +# +# NOTE: the binary is built with the `distro_binary` build tag, which is the +# packaged variant upstream ships (it drops the in-app self-update command). +pkgname=dms +version=1.4.6 +revision=1 +build_style=go +build_wrksrc="core" +go_import_path="github.com/AvengeMedia/DankMaterialShell/core" +go_package="${go_import_path}/cmd/dms" +go_build_tags="distro_binary" +go_ldflags="-X main.Version=${version}" +short_desc="DankMaterialShell — Material 3 desktop shell for Wayland" +maintainer="AvengeMedia " +license="MIT" +homepage="https://danklinux.com" +changelog="https://github.com/AvengeMedia/DankMaterialShell/releases" +distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${version}.tar.gz" +checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61 + +# Optional feature deps (XBPS has no "recommends") are listed in distro/void/README.md. +depends="quickshell accountsservice dgop matugen" + +post_install() { + # QML shell tree (build_style=go already installed the dms binary) + vmkdir usr/share/quickshell/dms + vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms + echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION" + + # Desktop entry + icon + vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications + vinstall "${wrksrc}/assets/danklogo.svg" 644 usr/share/icons/hicolor/scalable/apps + + # Shell completions (generated by the built binary; skip when cross-building) + vmkdir usr/share/bash-completion/completions + vmkdir usr/share/zsh/site-functions + vmkdir usr/share/fish/vendor_completions.d + if [ -z "$CROSS_BUILD" ]; then + "${DESTDIR}/usr/bin/dms" completion bash > "${DESTDIR}/usr/share/bash-completion/completions/dms" + "${DESTDIR}/usr/bin/dms" completion zsh > "${DESTDIR}/usr/share/zsh/site-functions/_dms" + "${DESTDIR}/usr/bin/dms" completion fish > "${DESTDIR}/usr/share/fish/vendor_completions.d/dms.fish" + fi + + vlicense "${wrksrc}/LICENSE" +} diff --git a/quickshell/Modules/Greetd/assets/dms-greeter b/quickshell/Modules/Greetd/assets/dms-greeter index 65e5aa90..55ac5914 100755 --- a/quickshell/Modules/Greetd/assets/dms-greeter +++ b/quickshell/Modules/Greetd/assets/dms-greeter @@ -227,6 +227,15 @@ export XDG_STATE_HOME="$CACHE_DIR/.local/state" export XDG_DATA_HOME="$CACHE_DIR/.local/share" export XDG_CACHE_HOME="$CACHE_DIR/.cache" +# Fallback runtime dir for systems without logind/pam_rundir (e.g. Void+seatd), +# where Wayland compositors abort with "RuntimeDirNotSet". Guarded so logind +# systems keep their own. +if [[ -z "${XDG_RUNTIME_DIR:-}" || ! -d "${XDG_RUNTIME_DIR:-}" ]]; then + export XDG_RUNTIME_DIR="$CACHE_DIR/run" + mkdir -p "$XDG_RUNTIME_DIR" + chmod 700 "$XDG_RUNTIME_DIR" +fi + # Keep greeter VT clean by default; callers can override via env or --debug. if [[ -z "${RUST_LOG:-}" ]]; then