fix(packaging): clean-tree dist, wayland and dbus deps, no personal paths, session export before clients
- commit
- 8d7d70018efa2d4cbc226f078355138f21e395ee
- author
- brice · · 2026-09-26 06:01 UTC
- parent
- 7abacfb
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +139 -35
M Makefile +28 -18
| @@ -47,25 +47,35 @@ arch: | |||
| 47 | 47 | check: vet test arch fakes-check | |
| 48 | 48 | ||
| 49 | 49 | # Self-contained source tarball for packaging (packaging/arch/PKGBUILD): the | |
| 50 | - | # sibling purego-* modules are vendored, so the package builds offline. | |
| 51 | - | # pacman-ordered: 0.0.0.r<commits>.g<hash>, a tag replaces 0.0.0 once there is one. | |
| 52 | - | VERSION ?= $(shell t=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//'); \ | |
| 53 | - | echo "$${t:-0.0.0}.r$$(git rev-list --count HEAD).g$$(git rev-parse --short HEAD)") | |
| 54 | - | DIST := dist/nefertty-$(VERSION) | |
| 50 | + | # sibling purego-* modules are vendored, so the package builds offline. It | |
| 51 | + | # holds the working trees; the version ends in .dirty when one is not clean. | |
| 55 | 52 | SIBLINGS := github.com/bnema/purego-libwayland github.com/bnema/purego-vulkan | |
| 53 | + | SIBLING_DIRS := $(shell go list -m -f '{{.Dir}}' $(SIBLINGS)) | |
| 54 | + | dirty = $(shell for d in . $(SIBLING_DIRS); do git -C $$d diff --quiet HEAD && \ | |
| 55 | + | test -z "$$(git -C $$d ls-files -o --exclude-standard)" || { echo .dirty; break; }; done) | |
| 56 | + | # pacman-ordered: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0. | |
| 57 | + | VERSION := $(or $(VERSION),$(shell t=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//; s/-/_/g'); \ | |
| 58 | + | echo "$${t:-0.0.0}.r$$(git rev-list --count HEAD).g$$(git rev-parse --short HEAD)")$(dirty)) | |
| 59 | + | DIST := dist/nefertty-$(VERSION) | |
| 60 | + | dist: SHELL := bash | |
| 61 | + | dist: .SHELLFLAGS := -eo pipefail -c | |
| 56 | 62 | dist: | |
| 57 | - | @rm -rf $(DIST) && mkdir -p $(DIST) | |
| 58 | - | git archive $$(git stash create || echo HEAD) | tar -x -C $(DIST) | |
| 59 | - | cd $(DIST) && go mod edit $$(cd $(CURDIR) && go list -m -f '-replace={{.Path}}={{.Dir}}' $(SIBLINGS)) && go mod vendor && \ | |
| 60 | - | for m in $(SIBLINGS); do d=$$(cd $(CURDIR) && go list -m -f '{{.Dir}}' $$m); \ | |
| 61 | - | sed -i "s#=> $$d\$$#=> ../$${m##*/}#" go.mod vendor/modules.txt; done && \ | |
| 62 | - | ! grep -rq "$(HOME)" go.mod vendor/modules.txt && echo '$(VERSION)' > VERSION | |
| 63 | - | tar -C dist -czf $(DIST).tar.gz nefertty-$(VERSION) | |
| 64 | - | @rm -rf $(DIST) && echo $(DIST).tar.gz | |
| 63 | + | @test -n "$(SIBLING_DIRS)" || { echo "sibling modules not found: $(SIBLINGS)" >&2; exit 1; } | |
| 64 | + | rm -rf $(DIST) && mkdir -p $(DIST) | |
| 65 | + | git ls-files -co --exclude-standard -z | tar --null -T - -c | tar -x -C $(DIST) | |
| 66 | + | cd $(DIST) && go mod edit $(foreach m,$(SIBLINGS),-replace=$(m)=$(shell go list -m -f '{{.Dir}}' $(m))) && go mod vendor | |
| 67 | + | cd $(DIST) && $(foreach m,$(SIBLINGS),sed -i 's#=> $(shell go list -m -f '{{.Dir}}' $(m))$$#=> ../$(notdir $(m))#' go.mod vendor/modules.txt &&) true | |
| 68 | + | @! grep -rqF "$(HOME)" $(DIST) || { echo "home path leaked into $(DIST)" >&2; exit 1; } | |
| 69 | + | echo '$(VERSION)' > $(DIST)/VERSION | |
| 70 | + | tar --owner=0 --group=0 --numeric-owner --sort=name --mtime=@$$(git log -1 --format=%ct) -C dist -czf $(DIST).tar.gz nefertty-$(VERSION) | |
| 71 | + | rm -rf $(DIST) | |
| 72 | + | @echo $(DIST).tar.gz | |
| 65 | 73 | ||
| 66 | - | # Arch package from the dist tarball; install it with `sudo pacman -U` on | |
| 67 | - | # the printed file. | |
| 74 | + | # Arch package from the dist tarball, built outside the home directory so | |
| 75 | + | # no path of it lands in the package metadata. Install it with | |
| 76 | + | # `sudo pacman -U` on the printed file. | |
| 68 | 77 | pkg: dist | |
| 69 | - | rm -rf dist/pkg && mkdir -p dist/pkg && cp packaging/arch/PKGBUILD $(DIST).tar.gz dist/pkg/ | |
| 70 | - | cd dist/pkg && sed -i "s/^pkgver=.*/pkgver=$(VERSION)/" PKGBUILD && makepkg -f --noconfirm | |
| 71 | - | @ls dist/pkg/*.pkg.tar.zst | |
| 78 | + | d=$$(mktemp -d /tmp/nefertty-pkg.XXXXXX) && cp packaging/arch/PKGBUILD $(DIST).tar.gz $$d/ && \ | |
| 79 | + | cd $$d && sed -i "s/^pkgver=.*/pkgver=$(VERSION)/; s/^sha256sums=.*/sha256sums=('$$(sha256sum *.tar.gz | cut -d' ' -f1)')/" PKGBUILD && \ | |
| 80 | + | makepkg -f --noconfirm && mv *.pkg.tar.zst $(CURDIR)/dist/ && cd / && rm -rf $$d | |
| 81 | + | @ls dist/*.pkg.tar.zst | |
| @@ -47,25 +47,35 @@ arch: | |||||
| 47 | check: vet test arch fakes-check | 47 | check: vet test arch fakes-check | ||
| 48 | 48 | ||||
| 49 | # Self-contained source tarball for packaging (packaging/arch/PKGBUILD): the | 49 | # Self-contained source tarball for packaging (packaging/arch/PKGBUILD): the | ||
| 50 | - | # sibling purego-* modules are vendored, so the package builds offline. | 50 | + | # sibling purego-* modules are vendored, so the package builds offline. It |
| 51 | - | # pacman-ordered: 0.0.0.r<commits>.g<hash>, a tag replaces 0.0.0 once there is one. | 51 | + | # holds the working trees; the version ends in .dirty when one is not clean. |
| 52 | - | VERSION ?= $(shell t=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//'); \ | |||
| 53 | - | echo "$${t:-0.0.0}.r$$(git rev-list --count HEAD).g$$(git rev-parse --short HEAD)") | |||
| 54 | - | DIST := dist/nefertty-$(VERSION) | |||
| 55 | SIBLINGS := github.com/bnema/purego-libwayland github.com/bnema/purego-vulkan | 52 | SIBLINGS := github.com/bnema/purego-libwayland github.com/bnema/purego-vulkan | ||
| 53 | + | SIBLING_DIRS := $(shell go list -m -f '{{.Dir}}' $(SIBLINGS)) | |||
| 54 | + | dirty = $(shell for d in . $(SIBLING_DIRS); do git -C $$d diff --quiet HEAD && \ | |||
| 55 | + | test -z "$$(git -C $$d ls-files -o --exclude-standard)" || { echo .dirty; break; }; done) | |||
| 56 | + | # pacman-ordered: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0. | |||
| 57 | + | VERSION := $(or $(VERSION),$(shell t=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//; s/-/_/g'); \ | |||
| 58 | + | echo "$${t:-0.0.0}.r$$(git rev-list --count HEAD).g$$(git rev-parse --short HEAD)")$(dirty)) | |||
| 59 | + | DIST := dist/nefertty-$(VERSION) | |||
| 60 | + | dist: SHELL := bash | |||
| 61 | + | dist: .SHELLFLAGS := -eo pipefail -c | |||
| 56 | dist: | 62 | dist: | ||
| 57 | - | @rm -rf $(DIST) && mkdir -p $(DIST) | 63 | + | @test -n "$(SIBLING_DIRS)" || { echo "sibling modules not found: $(SIBLINGS)" >&2; exit 1; } |
| 58 | - | git archive $$(git stash create || echo HEAD) | tar -x -C $(DIST) | 64 | + | rm -rf $(DIST) && mkdir -p $(DIST) |
| 59 | - | cd $(DIST) && go mod edit $$(cd $(CURDIR) && go list -m -f '-replace={{.Path}}={{.Dir}}' $(SIBLINGS)) && go mod vendor && \ | 65 | + | git ls-files -co --exclude-standard -z | tar --null -T - -c | tar -x -C $(DIST) |
| 60 | - | for m in $(SIBLINGS); do d=$$(cd $(CURDIR) && go list -m -f '{{.Dir}}' $$m); \ | 66 | + | cd $(DIST) && go mod edit $(foreach m,$(SIBLINGS),-replace=$(m)=$(shell go list -m -f '{{.Dir}}' $(m))) && go mod vendor |
| 61 | - | sed -i "s#=> $$d\$$#=> ../$${m##*/}#" go.mod vendor/modules.txt; done && \ | 67 | + | cd $(DIST) && $(foreach m,$(SIBLINGS),sed -i 's#=> $(shell go list -m -f '{{.Dir}}' $(m))$$#=> ../$(notdir $(m))#' go.mod vendor/modules.txt &&) true |
| 62 | - | ! grep -rq "$(HOME)" go.mod vendor/modules.txt && echo '$(VERSION)' > VERSION | 68 | + | @! grep -rqF "$(HOME)" $(DIST) || { echo "home path leaked into $(DIST)" >&2; exit 1; } |
| 63 | - | tar -C dist -czf $(DIST).tar.gz nefertty-$(VERSION) | 69 | + | echo '$(VERSION)' > $(DIST)/VERSION |
| 64 | - | @rm -rf $(DIST) && echo $(DIST).tar.gz | 70 | + | tar --owner=0 --group=0 --numeric-owner --sort=name --mtime=@$$(git log -1 --format=%ct) -C dist -czf $(DIST).tar.gz nefertty-$(VERSION) |
| 71 | + | rm -rf $(DIST) | |||
| 72 | + | @echo $(DIST).tar.gz | |||
| 65 | 73 | ||||
| 66 | - | # Arch package from the dist tarball; install it with `sudo pacman -U` on | 74 | + | # Arch package from the dist tarball, built outside the home directory so |
| 67 | - | # the printed file. | 75 | + | # no path of it lands in the package metadata. Install it with |
| 76 | + | # `sudo pacman -U` on the printed file. | |||
| 68 | pkg: dist | 77 | pkg: dist | ||
| 69 | - | rm -rf dist/pkg && mkdir -p dist/pkg && cp packaging/arch/PKGBUILD $(DIST).tar.gz dist/pkg/ | 78 | + | d=$$(mktemp -d /tmp/nefertty-pkg.XXXXXX) && cp packaging/arch/PKGBUILD $(DIST).tar.gz $$d/ && \ |
| 70 | - | cd dist/pkg && sed -i "s/^pkgver=.*/pkgver=$(VERSION)/" PKGBUILD && makepkg -f --noconfirm | 79 | + | cd $$d && sed -i "s/^pkgver=.*/pkgver=$(VERSION)/; s/^sha256sums=.*/sha256sums=('$$(sha256sum *.tar.gz | cut -d' ' -f1)')/" PKGBUILD && \ |
| 71 | - | @ls dist/pkg/*.pkg.tar.zst | 80 | + | makepkg -f --noconfirm && mv *.pkg.tar.zst $(CURDIR)/dist/ && cd / && rm -rf $$d |
| 81 | + | @ls dist/*.pkg.tar.zst | |||
M README.md +1 -1
| @@ -21,7 +21,7 @@ make pkg # builds dist/pkg/nefertty-*.pkg.tar.z | |||
| 21 | 21 | sudo pacman -U dist/pkg/nefertty-*.pkg.tar.zst | |
| 22 | 22 | ``` | |
| 23 | 23 | ||
| 24 | - | The package installs `nefertty` and a **NeferTTY** session for display managers (Ly, GDM, SDDM). The session runs `nefertty --session`, which shares `WAYLAND_DISPLAY` and `DISPLAY` with D-Bus and systemd user services, so portals and notification daemons reach it. `make dist` alone builds the self-contained source tarball (Go modules vendored) that the package uses. | |
| 24 | + | The package installs `nefertty` and a **NeferTTY** session for display managers (Ly, GDM, SDDM). The session runs `nefertty --session`, which shares `WAYLAND_DISPLAY` and `DISPLAY` with D-Bus and systemd user services, so portals and notification daemons reach it. `make dist` alone builds the self-contained source tarball (Go modules vendored) that the package uses; its version ends in `.dirty` when this repo or a sibling module has uncommitted changes. | |
| 25 | 25 | ||
| 26 | 26 | ## Configuration | |
| 27 | 27 | ||
| @@ -21,7 +21,7 @@ make pkg # builds dist/pkg/nefertty-*.pkg.tar.z | |||||
| 21 | sudo pacman -U dist/pkg/nefertty-*.pkg.tar.zst | 21 | sudo pacman -U dist/pkg/nefertty-*.pkg.tar.zst | ||
| 22 | ``` | 22 | ``` | ||
| 23 | 23 | ||||
| 24 | - | The package installs `nefertty` and a **NeferTTY** session for display managers (Ly, GDM, SDDM). The session runs `nefertty --session`, which shares `WAYLAND_DISPLAY` and `DISPLAY` with D-Bus and systemd user services, so portals and notification daemons reach it. `make dist` alone builds the self-contained source tarball (Go modules vendored) that the package uses. | 24 | + | The package installs `nefertty` and a **NeferTTY** session for display managers (Ly, GDM, SDDM). The session runs `nefertty --session`, which shares `WAYLAND_DISPLAY` and `DISPLAY` with D-Bus and systemd user services, so portals and notification daemons reach it. `make dist` alone builds the self-contained source tarball (Go modules vendored) that the package uses; its version ends in `.dirty` when this repo or a sibling module has uncommitted changes. |
| 25 | 25 | ||||
| 26 | ## Configuration | 26 | ## Configuration | ||
| 27 | 27 | ||||
M cmd/nefertty/main.go +1 -10
| @@ -23,10 +23,6 @@ import ( | |||
| 23 | 23 | ||
| 24 | 24 | type usageError struct{ error } | |
| 25 | 25 | ||
| 26 | - | // sessionEnvCommand exports the session environment to D-Bus and systemd | |
| 27 | - | // user services (--session). | |
| 28 | - | var sessionEnvCommand = []string{"dbus-update-activation-environment", "--systemd", "WAYLAND_DISPLAY", "DISPLAY", "XDG_CURRENT_DESKTOP", "XDG_SESSION_TYPE"} | |
| 29 | - | ||
| 30 | 26 | // version is set with -ldflags "-X main.version=..." by packaged builds. | |
| 31 | 27 | var version string | |
| 32 | 28 | ||
| @@ -190,11 +186,6 @@ func run() error { | |||
| 190 | 186 | fmt.Fprintln(os.Stderr, cfgErr) | |
| 191 | 187 | return cfgErr | |
| 192 | 188 | } | |
| 193 | - | if *session { | |
| 194 | - | // First startup command: portals and other D-Bus or systemd user | |
| 195 | - | // services started later find the session's displays. | |
| 196 | - | cfg.Startup = append([][]string{sessionEnvCommand}, cfg.Startup...) | |
| 197 | - | } | |
| 198 | 189 | var script *os.File | |
| 199 | 190 | if *inputPath != "" { | |
| 200 | 191 | if *inputPath == "-" { | |
| @@ -236,7 +227,7 @@ func run() error { | |||
| 236 | 227 | for _, w := range warnings { | |
| 237 | 228 | configLog.Warn().Int("line", w.Line).Msg(w.Msg) | |
| 238 | 229 | } | |
| 239 | - | err = app.Run(ctx, app.Options{Backend: *backend, Config: cfg, ConfigPath: path, Timeout: *timeout, NoTerminal: *noTerminal, NoXwayland: *noXwayland, ScreenshotDir: *screenshot, Sizes: sizes, Script: script}) | |
| 230 | + | err = app.Run(ctx, app.Options{Backend: *backend, Config: cfg, ConfigPath: path, Timeout: *timeout, NoTerminal: *noTerminal, NoXwayland: *noXwayland, Session: *session, ScreenshotDir: *screenshot, Sizes: sizes, Script: script}) | |
| 240 | 231 | // SIGINT and SIGTERM cancel the context and are clean exits. | |
| 241 | 232 | if err != nil && ctx.Err() != nil && errors.Is(err, context.Canceled) { | |
| 242 | 233 | err = nil | |
| @@ -23,10 +23,6 @@ import ( | |||||
| 23 | 23 | ||||
| 24 | type usageError struct{ error } | 24 | type usageError struct{ error } | ||
| 25 | 25 | ||||
| 26 | - | // sessionEnvCommand exports the session environment to D-Bus and systemd | |||
| 27 | - | // user services (--session). | |||
| 28 | - | var sessionEnvCommand = []string{"dbus-update-activation-environment", "--systemd", "WAYLAND_DISPLAY", "DISPLAY", "XDG_CURRENT_DESKTOP", "XDG_SESSION_TYPE"} | |||
| 29 | - | ||||
| 30 | // version is set with -ldflags "-X main.version=..." by packaged builds. | 26 | // version is set with -ldflags "-X main.version=..." by packaged builds. | ||
| 31 | var version string | 27 | var version string | ||
| 32 | 28 | ||||
| @@ -190,11 +186,6 @@ func run() error { | |||||
| 190 | fmt.Fprintln(os.Stderr, cfgErr) | 186 | fmt.Fprintln(os.Stderr, cfgErr) | ||
| 191 | return cfgErr | 187 | return cfgErr | ||
| 192 | } | 188 | } | ||
| 193 | - | if *session { | |||
| 194 | - | // First startup command: portals and other D-Bus or systemd user | |||
| 195 | - | // services started later find the session's displays. | |||
| 196 | - | cfg.Startup = append([][]string{sessionEnvCommand}, cfg.Startup...) | |||
| 197 | - | } | |||
| 198 | var script *os.File | 189 | var script *os.File | ||
| 199 | if *inputPath != "" { | 190 | if *inputPath != "" { | ||
| 200 | if *inputPath == "-" { | 191 | if *inputPath == "-" { | ||
| @@ -236,7 +227,7 @@ func run() error { | |||||
| 236 | for _, w := range warnings { | 227 | for _, w := range warnings { | ||
| 237 | configLog.Warn().Int("line", w.Line).Msg(w.Msg) | 228 | configLog.Warn().Int("line", w.Line).Msg(w.Msg) | ||
| 238 | } | 229 | } | ||
| 239 | - | err = app.Run(ctx, app.Options{Backend: *backend, Config: cfg, ConfigPath: path, Timeout: *timeout, NoTerminal: *noTerminal, NoXwayland: *noXwayland, ScreenshotDir: *screenshot, Sizes: sizes, Script: script}) | 230 | + | err = app.Run(ctx, app.Options{Backend: *backend, Config: cfg, ConfigPath: path, Timeout: *timeout, NoTerminal: *noTerminal, NoXwayland: *noXwayland, Session: *session, ScreenshotDir: *screenshot, Sizes: sizes, Script: script}) |
| 240 | // SIGINT and SIGTERM cancel the context and are clean exits. | 231 | // SIGINT and SIGTERM cancel the context and are clean exits. | ||
| 241 | if err != nil && ctx.Err() != nil && errors.Is(err, context.Canceled) { | 232 | if err != nil && ctx.Err() != nil && errors.Is(err, context.Canceled) { | ||
| 242 | err = nil | 233 | err = nil | ||
A internal/adapters/launcher/session.go +52 -0
| @@ -0,0 +1,52 @@ | |||
| 1 | + | package launcher | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "os/exec" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/zerowrap" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // sessionVars are the displays D-Bus and systemd user services (portals, | |
| 12 | + | // notification daemons) need to reach the session. | |
| 13 | + | var sessionVars = []string{"WAYLAND_DISPLAY", "DISPLAY", "XDG_CURRENT_DESKTOP", "XDG_SESSION_TYPE"} | |
| 14 | + | ||
| 15 | + | // ExportSession shares the session's displays with D-Bus and systemd user | |
| 16 | + | // services, and waits for it: clients started next may activate them. | |
| 17 | + | func ExportSession(ctx context.Context, env []string, log zerowrap.Logger) { | |
| 18 | + | ctx, cancel := context.WithTimeout(ctx, 5*time.Second) | |
| 19 | + | defer cancel() | |
| 20 | + | runSessionCommand(ctx, env, log, append([]string{"--systemd"}, sessionVars...), "session exported") | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // UnexportSession withdraws the displays from the systemd user manager on | |
| 24 | + | // exit, so services started after logout do not reach a dead display. | |
| 25 | + | func UnexportSession(env []string, log zerowrap.Logger) { | |
| 26 | + | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
| 27 | + | defer cancel() | |
| 28 | + | path, err := childLookPath("systemctl", env) | |
| 29 | + | if err != nil { | |
| 30 | + | return | |
| 31 | + | } | |
| 32 | + | cmd := exec.CommandContext(ctx, path, append([]string{"--user", "unset-environment"}, sessionVars...)...) | |
| 33 | + | cmd.Env = env | |
| 34 | + | if err := cmd.Run(); err != nil { | |
| 35 | + | log.Warn().Err(err).Msg("session unexport failed") | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | func runSessionCommand(ctx context.Context, env []string, log zerowrap.Logger, args []string, done string) { | |
| 40 | + | path, err := childLookPath("dbus-update-activation-environment", env) | |
| 41 | + | if err != nil { | |
| 42 | + | log.Warn().Err(err).Msg("session export skipped: dbus-update-activation-environment not found") | |
| 43 | + | return | |
| 44 | + | } | |
| 45 | + | cmd := exec.CommandContext(ctx, path, args...) | |
| 46 | + | cmd.Env = env | |
| 47 | + | if err := cmd.Run(); err != nil { | |
| 48 | + | log.Warn().Err(err).Msg("session export failed") | |
| 49 | + | return | |
| 50 | + | } | |
| 51 | + | log.Info().Strs("vars", sessionVars).Msg(done) | |
| 52 | + | } | |
| @@ -0,0 +1,52 @@ | |||||
| 1 | + | package launcher | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "os/exec" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/zerowrap" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // sessionVars are the displays D-Bus and systemd user services (portals, | |||
| 12 | + | // notification daemons) need to reach the session. | |||
| 13 | + | var sessionVars = []string{"WAYLAND_DISPLAY", "DISPLAY", "XDG_CURRENT_DESKTOP", "XDG_SESSION_TYPE"} | |||
| 14 | + | ||||
| 15 | + | // ExportSession shares the session's displays with D-Bus and systemd user | |||
| 16 | + | // services, and waits for it: clients started next may activate them. | |||
| 17 | + | func ExportSession(ctx context.Context, env []string, log zerowrap.Logger) { | |||
| 18 | + | ctx, cancel := context.WithTimeout(ctx, 5*time.Second) | |||
| 19 | + | defer cancel() | |||
| 20 | + | runSessionCommand(ctx, env, log, append([]string{"--systemd"}, sessionVars...), "session exported") | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // UnexportSession withdraws the displays from the systemd user manager on | |||
| 24 | + | // exit, so services started after logout do not reach a dead display. | |||
| 25 | + | func UnexportSession(env []string, log zerowrap.Logger) { | |||
| 26 | + | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |||
| 27 | + | defer cancel() | |||
| 28 | + | path, err := childLookPath("systemctl", env) | |||
| 29 | + | if err != nil { | |||
| 30 | + | return | |||
| 31 | + | } | |||
| 32 | + | cmd := exec.CommandContext(ctx, path, append([]string{"--user", "unset-environment"}, sessionVars...)...) | |||
| 33 | + | cmd.Env = env | |||
| 34 | + | if err := cmd.Run(); err != nil { | |||
| 35 | + | log.Warn().Err(err).Msg("session unexport failed") | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | func runSessionCommand(ctx context.Context, env []string, log zerowrap.Logger, args []string, done string) { | |||
| 40 | + | path, err := childLookPath("dbus-update-activation-environment", env) | |||
| 41 | + | if err != nil { | |||
| 42 | + | log.Warn().Err(err).Msg("session export skipped: dbus-update-activation-environment not found") | |||
| 43 | + | return | |||
| 44 | + | } | |||
| 45 | + | cmd := exec.CommandContext(ctx, path, args...) | |||
| 46 | + | cmd.Env = env | |||
| 47 | + | if err := cmd.Run(); err != nil { | |||
| 48 | + | log.Warn().Err(err).Msg("session export failed") | |||
| 49 | + | return | |||
| 50 | + | } | |||
| 51 | + | log.Info().Strs("vars", sessionVars).Msg(done) | |||
| 52 | + | } | |||
A internal/adapters/launcher/session_test.go +42 -0
| @@ -0,0 +1,42 @@ | |||
| 1 | + | package launcher | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "os" | |
| 6 | + | "path/filepath" | |
| 7 | + | "strings" | |
| 8 | + | "testing" | |
| 9 | + | ||
| 10 | + | "github.com/bnema/nefertty/internal/logging" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // The session commands run with the child environment and the right | |
| 14 | + | // arguments; the scripts on PATH record them. | |
| 15 | + | func TestExportSession(t *testing.T) { | |
| 16 | + | dir := t.TempDir() | |
| 17 | + | out := filepath.Join(dir, "calls") | |
| 18 | + | script := "#!/bin/sh\necho \"${0##*/} $* $WAYLAND_DISPLAY\" >> " + out + "\n" | |
| 19 | + | for _, name := range []string{"dbus-update-activation-environment", "systemctl"} { | |
| 20 | + | if err := os.WriteFile(filepath.Join(dir, name), []byte(script), 0o755); err != nil { | |
| 21 | + | t.Fatal(err) | |
| 22 | + | } | |
| 23 | + | } | |
| 24 | + | env := []string{"PATH=" + dir, "WAYLAND_DISPLAY=wayland-9"} | |
| 25 | + | ctx := context.Background() | |
| 26 | + | ExportSession(ctx, env, logging.For(ctx, "launcher")) | |
| 27 | + | UnexportSession(env, logging.For(ctx, "launcher")) | |
| 28 | + | data, err := os.ReadFile(out) | |
| 29 | + | if err != nil { | |
| 30 | + | t.Fatal(err) | |
| 31 | + | } | |
| 32 | + | want := "dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_TYPE wayland-9\n" + | |
| 33 | + | "systemctl --user unset-environment WAYLAND_DISPLAY DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_TYPE wayland-9\n" | |
| 34 | + | if string(data) != want { | |
| 35 | + | t.Fatalf("calls:\n%s", data) | |
| 36 | + | } | |
| 37 | + | // Missing tools are skipped, not fatal. | |
| 38 | + | ExportSession(ctx, []string{"PATH=" + t.TempDir()}, logging.For(ctx, "launcher")) | |
| 39 | + | if strings.Count(string(data), "\n") != 2 { | |
| 40 | + | t.Fatal(string(data)) | |
| 41 | + | } | |
| 42 | + | } | |
| @@ -0,0 +1,42 @@ | |||||
| 1 | + | package launcher | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "os" | |||
| 6 | + | "path/filepath" | |||
| 7 | + | "strings" | |||
| 8 | + | "testing" | |||
| 9 | + | ||||
| 10 | + | "github.com/bnema/nefertty/internal/logging" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // The session commands run with the child environment and the right | |||
| 14 | + | // arguments; the scripts on PATH record them. | |||
| 15 | + | func TestExportSession(t *testing.T) { | |||
| 16 | + | dir := t.TempDir() | |||
| 17 | + | out := filepath.Join(dir, "calls") | |||
| 18 | + | script := "#!/bin/sh\necho \"${0##*/} $* $WAYLAND_DISPLAY\" >> " + out + "\n" | |||
| 19 | + | for _, name := range []string{"dbus-update-activation-environment", "systemctl"} { | |||
| 20 | + | if err := os.WriteFile(filepath.Join(dir, name), []byte(script), 0o755); err != nil { | |||
| 21 | + | t.Fatal(err) | |||
| 22 | + | } | |||
| 23 | + | } | |||
| 24 | + | env := []string{"PATH=" + dir, "WAYLAND_DISPLAY=wayland-9"} | |||
| 25 | + | ctx := context.Background() | |||
| 26 | + | ExportSession(ctx, env, logging.For(ctx, "launcher")) | |||
| 27 | + | UnexportSession(env, logging.For(ctx, "launcher")) | |||
| 28 | + | data, err := os.ReadFile(out) | |||
| 29 | + | if err != nil { | |||
| 30 | + | t.Fatal(err) | |||
| 31 | + | } | |||
| 32 | + | want := "dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_TYPE wayland-9\n" + | |||
| 33 | + | "systemctl --user unset-environment WAYLAND_DISPLAY DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_TYPE wayland-9\n" | |||
| 34 | + | if string(data) != want { | |||
| 35 | + | t.Fatalf("calls:\n%s", data) | |||
| 36 | + | } | |||
| 37 | + | // Missing tools are skipped, not fatal. | |||
| 38 | + | ExportSession(ctx, []string{"PATH=" + t.TempDir()}, logging.For(ctx, "launcher")) | |||
| 39 | + | if strings.Count(string(data), "\n") != 2 { | |||
| 40 | + | t.Fatal(string(data)) | |||
| 41 | + | } | |||
| 42 | + | } | |||
M internal/app/app.go +8 -1
| @@ -34,7 +34,10 @@ type Options struct { | |||
| 34 | 34 | Timeout time.Duration | |
| 35 | 35 | NoTerminal bool | |
| 36 | 36 | // NoXwayland skips the X11 display even when the config enables it. | |
| 37 | - | NoXwayland bool | |
| 37 | + | NoXwayland bool | |
| 38 | + | // Session exports the displays to D-Bus and systemd user services | |
| 39 | + | // before any client starts, and withdraws them on exit. | |
| 40 | + | Session bool | |
| 38 | 41 | ScreenshotDir string | |
| 39 | 42 | // Sizes are the headless outputs (width, height), left to right; empty | |
| 40 | 43 | // means one 1920x1080 output. With several outputs, screenshots go to | |
| @@ -116,6 +119,10 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 116 | 119 | defer xdisplay.Close() | |
| 117 | 120 | childEnv = append(childEnv, "DISPLAY="+xdisplay.Name()) | |
| 118 | 121 | } | |
| 122 | + | if opts.Session { | |
| 123 | + | launcher.ExportSession(ctx, childEnv, logging.For(ctx, "launcher")) | |
| 124 | + | defer launcher.UnexportSession(childEnv, logging.For(ctx, "launcher")) | |
| 125 | + | } | |
| 119 | 126 | child := launcher.New(childEnv, logging.For(ctx, "launcher")) | |
| 120 | 127 | if inject != nil { | |
| 121 | 128 | inject(input) | |
| @@ -34,7 +34,10 @@ type Options struct { | |||||
| 34 | Timeout time.Duration | 34 | Timeout time.Duration | ||
| 35 | NoTerminal bool | 35 | NoTerminal bool | ||
| 36 | // NoXwayland skips the X11 display even when the config enables it. | 36 | // NoXwayland skips the X11 display even when the config enables it. | ||
| 37 | - | NoXwayland bool | 37 | + | NoXwayland bool |
| 38 | + | // Session exports the displays to D-Bus and systemd user services | |||
| 39 | + | // before any client starts, and withdraws them on exit. | |||
| 40 | + | Session bool | |||
| 38 | ScreenshotDir string | 41 | ScreenshotDir string | ||
| 39 | // Sizes are the headless outputs (width, height), left to right; empty | 42 | // Sizes are the headless outputs (width, height), left to right; empty | ||
| 40 | // means one 1920x1080 output. With several outputs, screenshots go to | 43 | // means one 1920x1080 output. With several outputs, screenshots go to | ||
| @@ -116,6 +119,10 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 116 | defer xdisplay.Close() | 119 | defer xdisplay.Close() | ||
| 117 | childEnv = append(childEnv, "DISPLAY="+xdisplay.Name()) | 120 | childEnv = append(childEnv, "DISPLAY="+xdisplay.Name()) | ||
| 118 | } | 121 | } | ||
| 122 | + | if opts.Session { | |||
| 123 | + | launcher.ExportSession(ctx, childEnv, logging.For(ctx, "launcher")) | |||
| 124 | + | defer launcher.UnexportSession(childEnv, logging.For(ctx, "launcher")) | |||
| 125 | + | } | |||
| 119 | child := launcher.New(childEnv, logging.For(ctx, "launcher")) | 126 | child := launcher.New(childEnv, logging.For(ctx, "launcher")) | ||
| 120 | if inject != nil { | 127 | if inject != nil { | ||
| 121 | inject(input) | 128 | inject(input) | ||
M packaging/arch/PKGBUILD +5 -5
| @@ -8,20 +8,19 @@ url='https://github.com/bnema/nefertty' | |||
| 8 | 8 | license=('MIT') | |
| 9 | 9 | # Loaded at runtime (purego, no cgo): not seen by makepkg's library scan. | |
| 10 | 10 | depends=('glibc' 'libinput' 'libxkbcommon' 'seatd' 'systemd-libs' 'vulkan-icd-loader' | |
| 11 | - | 'vulkan-driver') | |
| 11 | + | 'vulkan-driver' 'wayland' 'dbus') | |
| 12 | 12 | makedepends=('go') | |
| 13 | - | optdepends=('xwayland-satellite: X11 apps' | |
| 13 | + | optdepends=('xwayland-satellite>=0.7: X11 apps' | |
| 14 | 14 | 'foot: default terminal' | |
| 15 | 15 | 'fuzzel: launcher' | |
| 16 | 16 | 'xdg-desktop-portal-gtk: file pickers and settings for apps') | |
| 17 | - | options=('!debug') | |
| 18 | 17 | source=("$pkgname-$pkgver.tar.gz") | |
| 19 | 18 | sha256sums=('SKIP') | |
| 20 | 19 | ||
| 21 | 20 | build() { | |
| 22 | 21 | cd "$pkgname-$pkgver" | |
| 23 | - | export CGO_ENABLED=0 GOFLAGS='-mod=vendor -trimpath -buildvcs=false' GOPROXY=off | |
| 24 | - | go build -ldflags "-s -w -X main.version=$pkgver" -o nefertty ./cmd/nefertty | |
| 22 | + | export CGO_ENABLED=0 GOFLAGS='-mod=vendor -trimpath -buildvcs=false -buildmode=pie' GOPROXY=off GOTOOLCHAIN=local | |
| 23 | + | go build -ldflags "-X main.version=$pkgver" -o nefertty ./cmd/nefertty | |
| 25 | 24 | } | |
| 26 | 25 | ||
| 27 | 26 | check() { | |
| @@ -34,6 +33,7 @@ package() { | |||
| 34 | 33 | cd "$pkgname-$pkgver" | |
| 35 | 34 | install -Dm755 nefertty "$pkgdir/usr/bin/nefertty" | |
| 36 | 35 | install -Dm644 packaging/nefertty.desktop "$pkgdir/usr/share/wayland-sessions/nefertty.desktop" | |
| 36 | + | install -Dm644 packaging/nefertty-portals.conf "$pkgdir/usr/share/xdg-desktop-portal/nefertty-portals.conf" | |
| 37 | 37 | install -Dm644 examples/config "$pkgdir/usr/share/doc/nefertty/config.example" | |
| 38 | 38 | install -Dm644 README.md "$pkgdir/usr/share/doc/nefertty/README.md" | |
| 39 | 39 | install -Dm644 LICENSE "$pkgdir/usr/share/licenses/nefertty/LICENSE" | |
| @@ -8,20 +8,19 @@ url='https://github.com/bnema/nefertty' | |||||
| 8 | license=('MIT') | 8 | license=('MIT') | ||
| 9 | # Loaded at runtime (purego, no cgo): not seen by makepkg's library scan. | 9 | # Loaded at runtime (purego, no cgo): not seen by makepkg's library scan. | ||
| 10 | depends=('glibc' 'libinput' 'libxkbcommon' 'seatd' 'systemd-libs' 'vulkan-icd-loader' | 10 | depends=('glibc' 'libinput' 'libxkbcommon' 'seatd' 'systemd-libs' 'vulkan-icd-loader' | ||
| 11 | - | 'vulkan-driver') | 11 | + | 'vulkan-driver' 'wayland' 'dbus') |
| 12 | makedepends=('go') | 12 | makedepends=('go') | ||
| 13 | - | optdepends=('xwayland-satellite: X11 apps' | 13 | + | optdepends=('xwayland-satellite>=0.7: X11 apps' |
| 14 | 'foot: default terminal' | 14 | 'foot: default terminal' | ||
| 15 | 'fuzzel: launcher' | 15 | 'fuzzel: launcher' | ||
| 16 | 'xdg-desktop-portal-gtk: file pickers and settings for apps') | 16 | 'xdg-desktop-portal-gtk: file pickers and settings for apps') | ||
| 17 | - | options=('!debug') | |||
| 18 | source=("$pkgname-$pkgver.tar.gz") | 17 | source=("$pkgname-$pkgver.tar.gz") | ||
| 19 | sha256sums=('SKIP') | 18 | sha256sums=('SKIP') | ||
| 20 | 19 | ||||
| 21 | build() { | 20 | build() { | ||
| 22 | cd "$pkgname-$pkgver" | 21 | cd "$pkgname-$pkgver" | ||
| 23 | - | export CGO_ENABLED=0 GOFLAGS='-mod=vendor -trimpath -buildvcs=false' GOPROXY=off | 22 | + | export CGO_ENABLED=0 GOFLAGS='-mod=vendor -trimpath -buildvcs=false -buildmode=pie' GOPROXY=off GOTOOLCHAIN=local |
| 24 | - | go build -ldflags "-s -w -X main.version=$pkgver" -o nefertty ./cmd/nefertty | 23 | + | go build -ldflags "-X main.version=$pkgver" -o nefertty ./cmd/nefertty |
| 25 | } | 24 | } | ||
| 26 | 25 | ||||
| 27 | check() { | 26 | check() { | ||
| @@ -34,6 +33,7 @@ package() { | |||||
| 34 | cd "$pkgname-$pkgver" | 33 | cd "$pkgname-$pkgver" | ||
| 35 | install -Dm755 nefertty "$pkgdir/usr/bin/nefertty" | 34 | install -Dm755 nefertty "$pkgdir/usr/bin/nefertty" | ||
| 36 | install -Dm644 packaging/nefertty.desktop "$pkgdir/usr/share/wayland-sessions/nefertty.desktop" | 35 | install -Dm644 packaging/nefertty.desktop "$pkgdir/usr/share/wayland-sessions/nefertty.desktop" | ||
| 36 | + | install -Dm644 packaging/nefertty-portals.conf "$pkgdir/usr/share/xdg-desktop-portal/nefertty-portals.conf" | |||
| 37 | install -Dm644 examples/config "$pkgdir/usr/share/doc/nefertty/config.example" | 37 | install -Dm644 examples/config "$pkgdir/usr/share/doc/nefertty/config.example" | ||
| 38 | install -Dm644 README.md "$pkgdir/usr/share/doc/nefertty/README.md" | 38 | install -Dm644 README.md "$pkgdir/usr/share/doc/nefertty/README.md" | ||
| 39 | install -Dm644 LICENSE "$pkgdir/usr/share/licenses/nefertty/LICENSE" | 39 | install -Dm644 LICENSE "$pkgdir/usr/share/licenses/nefertty/LICENSE" | ||
A packaging/nefertty-portals.conf +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | [preferred] | |
| 2 | + | default=gtk | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | [preferred] | |||
| 2 | + | default=gtk | |||