Merge pull request #2 from bnema/feat/packaging
feat(packaging): add Arch PKGBUILDs, PAM service file and example config
9 files · +216 -4
M .gitignore +1 -0
| @@ -2,3 +2,4 @@ | |||
| 2 | 2 | bin/ | |
| 3 | 3 | *.pprof | |
| 4 | 4 | *.test | |
| 5 | + | /dist/ | |
| @@ -2,3 +2,4 @@ | |||||
| 2 | bin/ | 2 | bin/ | ||
| 3 | *.pprof | 3 | *.pprof | ||
| 4 | *.test | 4 | *.test | ||
| 5 | + | /dist/ | |||
M Makefile +21 -1
| @@ -1,4 +1,4 @@ | |||
| 1 | - | .PHONY: build test vet race mocks mocks-check fakes-check check | |
| 1 | + | .PHONY: build test vet race mocks mocks-check fakes-check check pkg install | |
| 2 | 2 | ||
| 3 | 3 | build: | |
| 4 | 4 | CGO_ENABLED=0 go build ./... | |
| @@ -18,3 +18,23 @@ fakes-check: | |||
| 18 | 18 | [ $$rc -eq 1 ] || { [ $$rc -eq 0 ] && printf '%s\n' 'handwritten test double: generate with Mockery v3' >&2; exit 1; } | |
| 19 | 19 | check: build vet test fakes-check | |
| 20 | 20 | CGO_ENABLED=0 staticcheck ./... | |
| 21 | + | ||
| 22 | + | # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules | |
| 23 | + | # come from the module proxy in prepare(); the build itself runs offline. | |
| 24 | + | # pacman-ordered version: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0. | |
| 25 | + | pkg: SHELL := bash | |
| 26 | + | pkg: .SHELLFLAGS := -eo pipefail -c | |
| 27 | + | pkg: | |
| 28 | + | @test -z "$$(git status --porcelain)" || echo "warning: uncommitted changes are not packaged" >&2 | |
| 29 | + | v=$$(t=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//; s/-/_/g'); \ | |
| 30 | + | echo "$${t:-0.0.0}.r$$(git rev-list --count HEAD).g$$(git rev-parse --short HEAD)"); \ | |
| 31 | + | d=$$(mktemp -d /tmp/neferafk-pkg.XXXXXX); trap 'rm -rf "$$d"' EXIT; \ | |
| 32 | + | git archive --prefix=neferafk-$$v/ -o "$$d/neferafk-$$v.tar.gz" HEAD; \ | |
| 33 | + | cp packaging/arch/PKGBUILD "$$d/"; \ | |
| 34 | + | cd "$$d" && sed -i "s/^pkgver=.*/pkgver=$$v/; s/^sha256sums=.*/sha256sums=('$$(sha256sum *.tar.gz | cut -d' ' -f1)')/" PKGBUILD; \ | |
| 35 | + | makepkg -f --noconfirm; mkdir -p $(CURDIR)/dist; rm -f $(CURDIR)/dist/neferafk-*.pkg.tar.zst; mv *.pkg.tar.zst $(CURDIR)/dist/ | |
| 36 | + | @ls dist/*.pkg.tar.zst | |
| 37 | + | ||
| 38 | + | # Build the package, then install it with pacman (asks for sudo). | |
| 39 | + | install: pkg | |
| 40 | + | sudo pacman -U dist/neferafk-*.pkg.tar.zst | |
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | .PHONY: build test vet race mocks mocks-check fakes-check check | 1 | + | .PHONY: build test vet race mocks mocks-check fakes-check check pkg install |
| 2 | 2 | ||||
| 3 | build: | 3 | build: | ||
| 4 | CGO_ENABLED=0 go build ./... | 4 | CGO_ENABLED=0 go build ./... | ||
| @@ -18,3 +18,23 @@ fakes-check: | |||||
| 18 | [ $$rc -eq 1 ] || { [ $$rc -eq 0 ] && printf '%s\n' 'handwritten test double: generate with Mockery v3' >&2; exit 1; } | 18 | [ $$rc -eq 1 ] || { [ $$rc -eq 0 ] && printf '%s\n' 'handwritten test double: generate with Mockery v3' >&2; exit 1; } | ||
| 19 | check: build vet test fakes-check | 19 | check: build vet test fakes-check | ||
| 20 | CGO_ENABLED=0 staticcheck ./... | 20 | CGO_ENABLED=0 staticcheck ./... | ||
| 21 | + | ||||
| 22 | + | # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules | |||
| 23 | + | # come from the module proxy in prepare(); the build itself runs offline. | |||
| 24 | + | # pacman-ordered version: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0. | |||
| 25 | + | pkg: SHELL := bash | |||
| 26 | + | pkg: .SHELLFLAGS := -eo pipefail -c | |||
| 27 | + | pkg: | |||
| 28 | + | @test -z "$$(git status --porcelain)" || echo "warning: uncommitted changes are not packaged" >&2 | |||
| 29 | + | v=$$(t=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//; s/-/_/g'); \ | |||
| 30 | + | echo "$${t:-0.0.0}.r$$(git rev-list --count HEAD).g$$(git rev-parse --short HEAD)"); \ | |||
| 31 | + | d=$$(mktemp -d /tmp/neferafk-pkg.XXXXXX); trap 'rm -rf "$$d"' EXIT; \ | |||
| 32 | + | git archive --prefix=neferafk-$$v/ -o "$$d/neferafk-$$v.tar.gz" HEAD; \ | |||
| 33 | + | cp packaging/arch/PKGBUILD "$$d/"; \ | |||
| 34 | + | cd "$$d" && sed -i "s/^pkgver=.*/pkgver=$$v/; s/^sha256sums=.*/sha256sums=('$$(sha256sum *.tar.gz | cut -d' ' -f1)')/" PKGBUILD; \ | |||
| 35 | + | makepkg -f --noconfirm; mkdir -p $(CURDIR)/dist; rm -f $(CURDIR)/dist/neferafk-*.pkg.tar.zst; mv *.pkg.tar.zst $(CURDIR)/dist/ | |||
| 36 | + | @ls dist/*.pkg.tar.zst | |||
| 37 | + | ||||
| 38 | + | # Build the package, then install it with pacman (asks for sudo). | |||
| 39 | + | install: pkg | |||
| 40 | + | sudo pacman -U dist/neferafk-*.pkg.tar.zst | |||
M README.md +9 -3
| @@ -16,13 +16,17 @@ It uses standard protocols only: `ext-idle-notify-v1`, `ext-session-lock-v1`, `w | |||
| 16 | 16 | ||
| 17 | 17 | - Linux with systemd-logind. | |
| 18 | 18 | - A compositor that advertises `ext_idle_notifier_v1` (v2), `ext_session_lock_manager_v1`, `zwp_linux_dmabuf_v1` (v4), `wp_linux_drm_syncobj_manager_v1`, and `zwlr_output_power_manager_v1` when screens off is enabled. | |
| 19 | - | - Password mode needs a PAM service file, `/etc/pam.d/neferafk`, for example: | |
| 19 | + | - Password mode needs a PAM service file, `/etc/pam.d/neferafk`. The Arch package installs it; otherwise create it with: | |
| 20 | 20 | ||
| 21 | 21 | ``` | |
| 22 | 22 | auth include login | |
| 23 | 23 | ``` | |
| 24 | 24 | ||
| 25 | - | ## Build | |
| 25 | + | ## Install | |
| 26 | + | ||
| 27 | + | Arch Linux: `make pkg` builds a package of the committed HEAD from `packaging/arch/PKGBUILD` into `dist/`, and `make install` installs it with pacman. `packaging/aur/neferafk-git/PKGBUILD` builds the latest `main`. | |
| 28 | + | ||
| 29 | + | From source: | |
| 26 | 30 | ||
| 27 | 31 | ```sh | |
| 28 | 32 | CGO_ENABLED=0 go build -o bin/neferafk ./cmd/neferafk | |
| @@ -34,13 +38,15 @@ CGO_ENABLED=0 go build -o bin/neferafk ./cmd/neferafk | |||
| 34 | 38 | neferafk run [--config FILE] run the daemon | |
| 35 | 39 | neferafk lock lock now | |
| 36 | 40 | neferafk status print the daemon state | |
| 41 | + | neferafk validate-config FILE check a configuration file | |
| 42 | + | neferafk version print the version | |
| 37 | 43 | ``` | |
| 38 | 44 | ||
| 39 | 45 | Start `neferafk run` from your compositor's startup. The daemon writes its log to `$XDG_STATE_HOME/neferafk/logs/daemon.log` (default `~/.local/state/neferafk/logs/daemon.log`). | |
| 40 | 46 | ||
| 41 | 47 | ## Configuration | |
| 42 | 48 | ||
| 43 | - | The file is `$XDG_CONFIG_HOME/neferafk/config` (default `~/.config/neferafk/config`). It uses flat `key = value` lines, and `#` starts a comment. Changes are applied live. Two cases need a daemon restart: enabling screens off, and enabling lock or sleep when both were off at startup. | |
| 49 | + | The file is `$XDG_CONFIG_HOME/neferafk/config` (default `~/.config/neferafk/config`); `examples/config` is a commented starting point. It uses flat `key = value` lines, and `#` starts a comment. Changes are applied live. Two cases need a daemon restart: enabling screens off, and enabling lock or sleep when both were off at startup. | |
| 44 | 50 | ||
| 45 | 51 | ``` | |
| 46 | 52 | fade.after = 6m | |
| @@ -16,13 +16,17 @@ It uses standard protocols only: `ext-idle-notify-v1`, `ext-session-lock-v1`, `w | |||||
| 16 | 16 | ||||
| 17 | - Linux with systemd-logind. | 17 | - Linux with systemd-logind. | ||
| 18 | - A compositor that advertises `ext_idle_notifier_v1` (v2), `ext_session_lock_manager_v1`, `zwp_linux_dmabuf_v1` (v4), `wp_linux_drm_syncobj_manager_v1`, and `zwlr_output_power_manager_v1` when screens off is enabled. | 18 | - A compositor that advertises `ext_idle_notifier_v1` (v2), `ext_session_lock_manager_v1`, `zwp_linux_dmabuf_v1` (v4), `wp_linux_drm_syncobj_manager_v1`, and `zwlr_output_power_manager_v1` when screens off is enabled. | ||
| 19 | - | - Password mode needs a PAM service file, `/etc/pam.d/neferafk`, for example: | 19 | + | - Password mode needs a PAM service file, `/etc/pam.d/neferafk`. The Arch package installs it; otherwise create it with: |
| 20 | 20 | ||||
| 21 | ``` | 21 | ``` | ||
| 22 | auth include login | 22 | auth include login | ||
| 23 | ``` | 23 | ``` | ||
| 24 | 24 | ||||
| 25 | - | ## Build | 25 | + | ## Install |
| 26 | + | ||||
| 27 | + | Arch Linux: `make pkg` builds a package of the committed HEAD from `packaging/arch/PKGBUILD` into `dist/`, and `make install` installs it with pacman. `packaging/aur/neferafk-git/PKGBUILD` builds the latest `main`. | |||
| 28 | + | ||||
| 29 | + | From source: | |||
| 26 | 30 | ||||
| 27 | ```sh | 31 | ```sh | ||
| 28 | CGO_ENABLED=0 go build -o bin/neferafk ./cmd/neferafk | 32 | CGO_ENABLED=0 go build -o bin/neferafk ./cmd/neferafk | ||
| @@ -34,13 +38,15 @@ CGO_ENABLED=0 go build -o bin/neferafk ./cmd/neferafk | |||||
| 34 | neferafk run [--config FILE] run the daemon | 38 | neferafk run [--config FILE] run the daemon | ||
| 35 | neferafk lock lock now | 39 | neferafk lock lock now | ||
| 36 | neferafk status print the daemon state | 40 | neferafk status print the daemon state | ||
| 41 | + | neferafk validate-config FILE check a configuration file | |||
| 42 | + | neferafk version print the version | |||
| 37 | ``` | 43 | ``` | ||
| 38 | 44 | ||||
| 39 | Start `neferafk run` from your compositor's startup. The daemon writes its log to `$XDG_STATE_HOME/neferafk/logs/daemon.log` (default `~/.local/state/neferafk/logs/daemon.log`). | 45 | Start `neferafk run` from your compositor's startup. The daemon writes its log to `$XDG_STATE_HOME/neferafk/logs/daemon.log` (default `~/.local/state/neferafk/logs/daemon.log`). | ||
| 40 | 46 | ||||
| 41 | ## Configuration | 47 | ## Configuration | ||
| 42 | 48 | ||||
| 43 | - | The file is `$XDG_CONFIG_HOME/neferafk/config` (default `~/.config/neferafk/config`). It uses flat `key = value` lines, and `#` starts a comment. Changes are applied live. Two cases need a daemon restart: enabling screens off, and enabling lock or sleep when both were off at startup. | 49 | + | The file is `$XDG_CONFIG_HOME/neferafk/config` (default `~/.config/neferafk/config`); `examples/config` is a commented starting point. It uses flat `key = value` lines, and `#` starts a comment. Changes are applied live. Two cases need a daemon restart: enabling screens off, and enabling lock or sleep when both were off at startup. |
| 44 | 50 | ||||
| 45 | ``` | 51 | ``` | ||
| 46 | fade.after = 6m | 52 | fade.after = 6m | ||
M cmd/neferafk/main.go +28 -0
| @@ -7,11 +7,13 @@ import ( | |||
| 7 | 7 | "flag" | |
| 8 | 8 | "fmt" | |
| 9 | 9 | "io" | |
| 10 | + | "io/fs" | |
| 10 | 11 | "os" | |
| 11 | 12 | "os/signal" | |
| 12 | 13 | "syscall" | |
| 13 | 14 | ||
| 14 | 15 | "github.com/bnema/neferafk/internal/adapters/auth" | |
| 16 | + | "github.com/bnema/neferafk/internal/adapters/config" | |
| 15 | 17 | "github.com/bnema/neferafk/internal/adapters/control" | |
| 16 | 18 | "github.com/bnema/neferafk/internal/adapters/visual" | |
| 17 | 19 | "github.com/bnema/neferafk/internal/app" | |
| @@ -25,12 +27,17 @@ const ( | |||
| 25 | 27 | exitUnsupported = 3 | |
| 26 | 28 | ) | |
| 27 | 29 | ||
| 30 | + | // version is set at build time with -ldflags "-X main.version=...". | |
| 31 | + | var version = "dev" | |
| 32 | + | ||
| 28 | 33 | const usage = `usage: neferafk <command> [flags] | |
| 29 | 34 | ||
| 30 | 35 | commands: | |
| 31 | 36 | run [--config FILE] run the absence daemon | |
| 32 | 37 | lock ask the running daemon to lock now | |
| 33 | 38 | status print the running daemon's state | |
| 39 | + | validate-config FILE check a configuration file and exit | |
| 40 | + | version print the version | |
| 34 | 41 | visual internal: fade/lock process (fd 3 commands in, fd 4 events out) | |
| 35 | 42 | ` | |
| 36 | 43 | ||
| @@ -51,6 +58,27 @@ func execute(args []string, stdout, stderr io.Writer) int { | |||
| 51 | 58 | return runVisual(rest, os.NewFile(3, "visual-cmd"), os.NewFile(4, "visual-events"), stderr) | |
| 52 | 59 | case "auth-worker": // hidden: spawned by the visual process only | |
| 53 | 60 | return runAuthWorker(rest, os.NewFile(3, "auth-in"), os.NewFile(4, "auth-out"), stderr) | |
| 61 | + | case "version": | |
| 62 | + | if len(rest) != 0 { | |
| 63 | + | fmt.Fprint(stderr, usage) | |
| 64 | + | return exitUsage | |
| 65 | + | } | |
| 66 | + | fmt.Fprintln(stdout, version) | |
| 67 | + | return exitOK | |
| 68 | + | case "validate-config": | |
| 69 | + | if len(rest) != 1 { | |
| 70 | + | fmt.Fprint(stderr, usage) | |
| 71 | + | return exitUsage | |
| 72 | + | } | |
| 73 | + | if _, err := config.Load(rest[0]); err != nil { | |
| 74 | + | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) { | |
| 75 | + | fmt.Fprintf(stderr, "neferafk: %v\n", err) // already names the file | |
| 76 | + | } else { | |
| 77 | + | fmt.Fprintf(stderr, "neferafk: %s: %v\n", rest[0], err) | |
| 78 | + | } | |
| 79 | + | return exitFailure | |
| 80 | + | } | |
| 81 | + | return exitOK | |
| 54 | 82 | case "-h", "--help", "help": | |
| 55 | 83 | fmt.Fprint(stdout, usage) | |
| 56 | 84 | return exitOK | |
| @@ -7,11 +7,13 @@ import ( | |||||
| 7 | "flag" | 7 | "flag" | ||
| 8 | "fmt" | 8 | "fmt" | ||
| 9 | "io" | 9 | "io" | ||
| 10 | + | "io/fs" | |||
| 10 | "os" | 11 | "os" | ||
| 11 | "os/signal" | 12 | "os/signal" | ||
| 12 | "syscall" | 13 | "syscall" | ||
| 13 | 14 | ||||
| 14 | "github.com/bnema/neferafk/internal/adapters/auth" | 15 | "github.com/bnema/neferafk/internal/adapters/auth" | ||
| 16 | + | "github.com/bnema/neferafk/internal/adapters/config" | |||
| 15 | "github.com/bnema/neferafk/internal/adapters/control" | 17 | "github.com/bnema/neferafk/internal/adapters/control" | ||
| 16 | "github.com/bnema/neferafk/internal/adapters/visual" | 18 | "github.com/bnema/neferafk/internal/adapters/visual" | ||
| 17 | "github.com/bnema/neferafk/internal/app" | 19 | "github.com/bnema/neferafk/internal/app" | ||
| @@ -25,12 +27,17 @@ const ( | |||||
| 25 | exitUnsupported = 3 | 27 | exitUnsupported = 3 | ||
| 26 | ) | 28 | ) | ||
| 27 | 29 | ||||
| 30 | + | // version is set at build time with -ldflags "-X main.version=...". | |||
| 31 | + | var version = "dev" | |||
| 32 | + | ||||
| 28 | const usage = `usage: neferafk <command> [flags] | 33 | const usage = `usage: neferafk <command> [flags] | ||
| 29 | 34 | ||||
| 30 | commands: | 35 | commands: | ||
| 31 | run [--config FILE] run the absence daemon | 36 | run [--config FILE] run the absence daemon | ||
| 32 | lock ask the running daemon to lock now | 37 | lock ask the running daemon to lock now | ||
| 33 | status print the running daemon's state | 38 | status print the running daemon's state | ||
| 39 | + | validate-config FILE check a configuration file and exit | |||
| 40 | + | version print the version | |||
| 34 | visual internal: fade/lock process (fd 3 commands in, fd 4 events out) | 41 | visual internal: fade/lock process (fd 3 commands in, fd 4 events out) | ||
| 35 | ` | 42 | ` | ||
| 36 | 43 | ||||
| @@ -51,6 +58,27 @@ func execute(args []string, stdout, stderr io.Writer) int { | |||||
| 51 | return runVisual(rest, os.NewFile(3, "visual-cmd"), os.NewFile(4, "visual-events"), stderr) | 58 | return runVisual(rest, os.NewFile(3, "visual-cmd"), os.NewFile(4, "visual-events"), stderr) | ||
| 52 | case "auth-worker": // hidden: spawned by the visual process only | 59 | case "auth-worker": // hidden: spawned by the visual process only | ||
| 53 | return runAuthWorker(rest, os.NewFile(3, "auth-in"), os.NewFile(4, "auth-out"), stderr) | 60 | return runAuthWorker(rest, os.NewFile(3, "auth-in"), os.NewFile(4, "auth-out"), stderr) | ||
| 61 | + | case "version": | |||
| 62 | + | if len(rest) != 0 { | |||
| 63 | + | fmt.Fprint(stderr, usage) | |||
| 64 | + | return exitUsage | |||
| 65 | + | } | |||
| 66 | + | fmt.Fprintln(stdout, version) | |||
| 67 | + | return exitOK | |||
| 68 | + | case "validate-config": | |||
| 69 | + | if len(rest) != 1 { | |||
| 70 | + | fmt.Fprint(stderr, usage) | |||
| 71 | + | return exitUsage | |||
| 72 | + | } | |||
| 73 | + | if _, err := config.Load(rest[0]); err != nil { | |||
| 74 | + | if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) { | |||
| 75 | + | fmt.Fprintf(stderr, "neferafk: %v\n", err) // already names the file | |||
| 76 | + | } else { | |||
| 77 | + | fmt.Fprintf(stderr, "neferafk: %s: %v\n", rest[0], err) | |||
| 78 | + | } | |||
| 79 | + | return exitFailure | |||
| 80 | + | } | |||
| 81 | + | return exitOK | |||
| 54 | case "-h", "--help", "help": | 82 | case "-h", "--help", "help": | ||
| 55 | fmt.Fprint(stdout, usage) | 83 | fmt.Fprint(stdout, usage) | ||
| 56 | return exitOK | 84 | return exitOK | ||
M cmd/neferafk/main_test.go +36 -0
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "golang.org/x/sys/unix" | |
| 7 | 7 | "os" | |
| 8 | 8 | "os/exec" | |
| 9 | + | "path/filepath" | |
| 9 | 10 | "strings" | |
| 10 | 11 | "syscall" | |
| 11 | 12 | "testing" | |
| @@ -86,6 +87,41 @@ func TestRunRejectsInvalidConfig(t *testing.T) { | |||
| 86 | 87 | } | |
| 87 | 88 | } | |
| 88 | 89 | ||
| 90 | + | func TestVersionAndValidateConfig(t *testing.T) { | |
| 91 | + | if code, o, _ := run(t, "version"); code != exitOK || strings.TrimSpace(o) != version { | |
| 92 | + | t.Fatalf("version: %d %q", code, o) | |
| 93 | + | } | |
| 94 | + | if code, _, _ := run(t, "version", "extra"); code != exitUsage { | |
| 95 | + | t.Fatal("version accepted an argument") | |
| 96 | + | } | |
| 97 | + | bad := filepath.Join(t.TempDir(), "config") | |
| 98 | + | if err := writeFile(bad, "lock.after = banana\n"); err != nil { | |
| 99 | + | t.Fatal(err) | |
| 100 | + | } | |
| 101 | + | missing := filepath.Join(t.TempDir(), "missing") | |
| 102 | + | for _, tc := range []struct { | |
| 103 | + | name string | |
| 104 | + | args []string | |
| 105 | + | code int | |
| 106 | + | stderr string | |
| 107 | + | }{ | |
| 108 | + | {"example", []string{"validate-config", "../../examples/config"}, exitOK, ""}, | |
| 109 | + | {"invalid", []string{"validate-config", bad}, exitFailure, "line 1"}, | |
| 110 | + | {"missing", []string{"validate-config", missing}, exitFailure, "no such file"}, | |
| 111 | + | {"no file", []string{"validate-config"}, exitUsage, "usage:"}, | |
| 112 | + | } { | |
| 113 | + | t.Run(tc.name, func(t *testing.T) { | |
| 114 | + | code, _, e := run(t, tc.args...) | |
| 115 | + | if code != tc.code || !strings.Contains(e, tc.stderr) { | |
| 116 | + | t.Fatalf("%d %q", code, e) | |
| 117 | + | } | |
| 118 | + | if tc.name == "missing" && strings.Count(e, missing) != 1 { | |
| 119 | + | t.Fatalf("path not named exactly once: %q", e) | |
| 120 | + | } | |
| 121 | + | }) | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | ||
| 89 | 125 | func TestServeAuthRunsWorkerOnPipes(t *testing.T) { | |
| 90 | 126 | var in, out, errb bytes.Buffer | |
| 91 | 127 | if err := auth.WriteBootstrap(&in, ports.AuthBootstrap{Generation: 3, PINSource: ports.PINSourceEnv, EnvPIN: []byte("123456")}); err != nil { | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "golang.org/x/sys/unix" | 6 | "golang.org/x/sys/unix" | ||
| 7 | "os" | 7 | "os" | ||
| 8 | "os/exec" | 8 | "os/exec" | ||
| 9 | + | "path/filepath" | |||
| 9 | "strings" | 10 | "strings" | ||
| 10 | "syscall" | 11 | "syscall" | ||
| 11 | "testing" | 12 | "testing" | ||
| @@ -86,6 +87,41 @@ func TestRunRejectsInvalidConfig(t *testing.T) { | |||||
| 86 | } | 87 | } | ||
| 87 | } | 88 | } | ||
| 88 | 89 | ||||
| 90 | + | func TestVersionAndValidateConfig(t *testing.T) { | |||
| 91 | + | if code, o, _ := run(t, "version"); code != exitOK || strings.TrimSpace(o) != version { | |||
| 92 | + | t.Fatalf("version: %d %q", code, o) | |||
| 93 | + | } | |||
| 94 | + | if code, _, _ := run(t, "version", "extra"); code != exitUsage { | |||
| 95 | + | t.Fatal("version accepted an argument") | |||
| 96 | + | } | |||
| 97 | + | bad := filepath.Join(t.TempDir(), "config") | |||
| 98 | + | if err := writeFile(bad, "lock.after = banana\n"); err != nil { | |||
| 99 | + | t.Fatal(err) | |||
| 100 | + | } | |||
| 101 | + | missing := filepath.Join(t.TempDir(), "missing") | |||
| 102 | + | for _, tc := range []struct { | |||
| 103 | + | name string | |||
| 104 | + | args []string | |||
| 105 | + | code int | |||
| 106 | + | stderr string | |||
| 107 | + | }{ | |||
| 108 | + | {"example", []string{"validate-config", "../../examples/config"}, exitOK, ""}, | |||
| 109 | + | {"invalid", []string{"validate-config", bad}, exitFailure, "line 1"}, | |||
| 110 | + | {"missing", []string{"validate-config", missing}, exitFailure, "no such file"}, | |||
| 111 | + | {"no file", []string{"validate-config"}, exitUsage, "usage:"}, | |||
| 112 | + | } { | |||
| 113 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 114 | + | code, _, e := run(t, tc.args...) | |||
| 115 | + | if code != tc.code || !strings.Contains(e, tc.stderr) { | |||
| 116 | + | t.Fatalf("%d %q", code, e) | |||
| 117 | + | } | |||
| 118 | + | if tc.name == "missing" && strings.Count(e, missing) != 1 { | |||
| 119 | + | t.Fatalf("path not named exactly once: %q", e) | |||
| 120 | + | } | |||
| 121 | + | }) | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | ||||
| 89 | func TestServeAuthRunsWorkerOnPipes(t *testing.T) { | 125 | func TestServeAuthRunsWorkerOnPipes(t *testing.T) { | ||
| 90 | var in, out, errb bytes.Buffer | 126 | var in, out, errb bytes.Buffer | ||
| 91 | if err := auth.WriteBootstrap(&in, ports.AuthBootstrap{Generation: 3, PINSource: ports.PINSourceEnv, EnvPIN: []byte("123456")}); err != nil { | 127 | if err := auth.WriteBootstrap(&in, ports.AuthBootstrap{Generation: 3, PINSource: ports.PINSourceEnv, EnvPIN: []byte("123456")}); err != nil { | ||
A examples/config +18 -0
| @@ -0,0 +1,18 @@ | |||
| 1 | + | # NeferAFK configuration: copy to ~/.config/neferafk/config. | |
| 2 | + | # Delays count from the last input. `off` disables an action. | |
| 3 | + | # Changes apply live, except enabling screens off, or enabling lock or sleep | |
| 4 | + | # when both were off at startup: those need a daemon restart. | |
| 5 | + | ||
| 6 | + | fade.after = 6m | |
| 7 | + | fade.duration = 2s | |
| 8 | + | lock.after = 5m | |
| 9 | + | screens.off-after = 7m | |
| 10 | + | sleep.after = 20m | |
| 11 | + | ||
| 12 | + | # password (PAM service /etc/pam.d/neferafk) or pin. | |
| 13 | + | auth.mode = password | |
| 14 | + | # PIN mode: 6 to 32 digits from `pass show ENTRY` (or auth.pin-source = env | |
| 15 | + | # with auth.pin-env = VARIABLE). Replace auth.mode above with: | |
| 16 | + | # auth.mode = pin | |
| 17 | + | # auth.pin-source = pass | |
| 18 | + | # auth.pin-entry = neferafk/pin | |
| @@ -0,0 +1,18 @@ | |||||
| 1 | + | # NeferAFK configuration: copy to ~/.config/neferafk/config. | |||
| 2 | + | # Delays count from the last input. `off` disables an action. | |||
| 3 | + | # Changes apply live, except enabling screens off, or enabling lock or sleep | |||
| 4 | + | # when both were off at startup: those need a daemon restart. | |||
| 5 | + | ||||
| 6 | + | fade.after = 6m | |||
| 7 | + | fade.duration = 2s | |||
| 8 | + | lock.after = 5m | |||
| 9 | + | screens.off-after = 7m | |||
| 10 | + | sleep.after = 20m | |||
| 11 | + | ||||
| 12 | + | # password (PAM service /etc/pam.d/neferafk) or pin. | |||
| 13 | + | auth.mode = password | |||
| 14 | + | # PIN mode: 6 to 32 digits from `pass show ENTRY` (or auth.pin-source = env | |||
| 15 | + | # with auth.pin-env = VARIABLE). Replace auth.mode above with: | |||
| 16 | + | # auth.mode = pin | |||
| 17 | + | # auth.pin-source = pass | |||
| 18 | + | # auth.pin-entry = neferafk/pin | |||
A packaging/arch/PKGBUILD +44 -0
| @@ -0,0 +1,44 @@ | |||
| 1 | + | # Built by `make pkg` at the repo root, from `git archive HEAD`. | |
| 2 | + | pkgname=neferafk | |
| 3 | + | pkgver=0.0.0 | |
| 4 | + | pkgrel=1 | |
| 5 | + | pkgdesc='Idle daemon for Wayland: fade, session lock, screens off and suspend' | |
| 6 | + | arch=('x86_64') | |
| 7 | + | url='https://github.com/bnema/neferafk' | |
| 8 | + | license=('GPL-3.0-only') | |
| 9 | + | # Loaded at runtime (purego, no cgo): not seen by makepkg's library scan. | |
| 10 | + | depends=('glibc' 'pam' 'libxkbcommon' 'vulkan-icd-loader' 'vulkan-driver' 'systemd' | |
| 11 | + | 'ttf-dejavu') # lock screen monospace font (DejaVu Sans Mono) | |
| 12 | + | makedepends=('go>=2:1.27') | |
| 13 | + | optdepends=('pass: PIN source for auth.mode = pin') | |
| 14 | + | backup=('etc/pam.d/neferafk') | |
| 15 | + | source=("$pkgname-$pkgver.tar.gz") | |
| 16 | + | sha256sums=('SKIP') | |
| 17 | + | ||
| 18 | + | prepare() { | |
| 19 | + | cd "$pkgname-$pkgver" | |
| 20 | + | export GOPATH="$srcdir/gopath" GOFLAGS='-mod=readonly -modcacherw' GOWORK=off GOTOOLCHAIN=local | |
| 21 | + | go mod download | |
| 22 | + | go mod vendor | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | build() { | |
| 26 | + | cd "$pkgname-$pkgver" | |
| 27 | + | export CGO_ENABLED=0 GOFLAGS='-mod=vendor -trimpath -buildvcs=false -buildmode=pie' GOPROXY=off GOWORK=off GOTOOLCHAIN=local | |
| 28 | + | go build -ldflags "-X main.version=$pkgver" -o neferafk ./cmd/neferafk | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | check() { | |
| 32 | + | cd "$pkgname-$pkgver" | |
| 33 | + | ./neferafk version | |
| 34 | + | ./neferafk validate-config examples/config | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | package() { | |
| 38 | + | cd "$pkgname-$pkgver" | |
| 39 | + | install -Dm755 neferafk "$pkgdir/usr/bin/neferafk" | |
| 40 | + | install -Dm644 packaging/neferafk.pam "$pkgdir/etc/pam.d/neferafk" | |
| 41 | + | install -Dm644 examples/config "$pkgdir/usr/share/doc/neferafk/config.example" | |
| 42 | + | install -Dm644 README.md "$pkgdir/usr/share/doc/neferafk/README.md" | |
| 43 | + | install -Dm644 LICENSE "$pkgdir/usr/share/licenses/neferafk/LICENSE" | |
| 44 | + | } | |
| @@ -0,0 +1,44 @@ | |||||
| 1 | + | # Built by `make pkg` at the repo root, from `git archive HEAD`. | |||
| 2 | + | pkgname=neferafk | |||
| 3 | + | pkgver=0.0.0 | |||
| 4 | + | pkgrel=1 | |||
| 5 | + | pkgdesc='Idle daemon for Wayland: fade, session lock, screens off and suspend' | |||
| 6 | + | arch=('x86_64') | |||
| 7 | + | url='https://github.com/bnema/neferafk' | |||
| 8 | + | license=('GPL-3.0-only') | |||
| 9 | + | # Loaded at runtime (purego, no cgo): not seen by makepkg's library scan. | |||
| 10 | + | depends=('glibc' 'pam' 'libxkbcommon' 'vulkan-icd-loader' 'vulkan-driver' 'systemd' | |||
| 11 | + | 'ttf-dejavu') # lock screen monospace font (DejaVu Sans Mono) | |||
| 12 | + | makedepends=('go>=2:1.27') | |||
| 13 | + | optdepends=('pass: PIN source for auth.mode = pin') | |||
| 14 | + | backup=('etc/pam.d/neferafk') | |||
| 15 | + | source=("$pkgname-$pkgver.tar.gz") | |||
| 16 | + | sha256sums=('SKIP') | |||
| 17 | + | ||||
| 18 | + | prepare() { | |||
| 19 | + | cd "$pkgname-$pkgver" | |||
| 20 | + | export GOPATH="$srcdir/gopath" GOFLAGS='-mod=readonly -modcacherw' GOWORK=off GOTOOLCHAIN=local | |||
| 21 | + | go mod download | |||
| 22 | + | go mod vendor | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | build() { | |||
| 26 | + | cd "$pkgname-$pkgver" | |||
| 27 | + | export CGO_ENABLED=0 GOFLAGS='-mod=vendor -trimpath -buildvcs=false -buildmode=pie' GOPROXY=off GOWORK=off GOTOOLCHAIN=local | |||
| 28 | + | go build -ldflags "-X main.version=$pkgver" -o neferafk ./cmd/neferafk | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | check() { | |||
| 32 | + | cd "$pkgname-$pkgver" | |||
| 33 | + | ./neferafk version | |||
| 34 | + | ./neferafk validate-config examples/config | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | package() { | |||
| 38 | + | cd "$pkgname-$pkgver" | |||
| 39 | + | install -Dm755 neferafk "$pkgdir/usr/bin/neferafk" | |||
| 40 | + | install -Dm644 packaging/neferafk.pam "$pkgdir/etc/pam.d/neferafk" | |||
| 41 | + | install -Dm644 examples/config "$pkgdir/usr/share/doc/neferafk/config.example" | |||
| 42 | + | install -Dm644 README.md "$pkgdir/usr/share/doc/neferafk/README.md" | |||
| 43 | + | install -Dm644 LICENSE "$pkgdir/usr/share/licenses/neferafk/LICENSE" | |||
| 44 | + | } | |||
A packaging/aur/neferafk-git/PKGBUILD +53 -0
| @@ -0,0 +1,53 @@ | |||
| 1 | + | # Maintainer: bnema <b at bnema dot dev> | |
| 2 | + | pkgname=neferafk-git | |
| 3 | + | pkgver=0.1.0.r0.g07cdc18 | |
| 4 | + | pkgrel=1 | |
| 5 | + | pkgdesc='Idle daemon for Wayland: fade, session lock, screens off and suspend (git version)' | |
| 6 | + | arch=('x86_64' 'aarch64') | |
| 7 | + | url='https://github.com/bnema/neferafk' | |
| 8 | + | license=('GPL-3.0-only') | |
| 9 | + | # Loaded at runtime (purego, no cgo): not seen by makepkg's library scan. | |
| 10 | + | depends=('glibc' 'pam' 'libxkbcommon' 'vulkan-icd-loader' 'vulkan-driver' 'systemd' | |
| 11 | + | 'ttf-dejavu') # lock screen monospace font (DejaVu Sans Mono) | |
| 12 | + | makedepends=('go>=2:1.27' 'git') | |
| 13 | + | optdepends=('pass: PIN source for auth.mode = pin') | |
| 14 | + | provides=('neferafk') | |
| 15 | + | conflicts=('neferafk') | |
| 16 | + | backup=('etc/pam.d/neferafk') | |
| 17 | + | source=('git+https://github.com/bnema/neferafk.git') | |
| 18 | + | sha256sums=('SKIP') | |
| 19 | + | ||
| 20 | + | pkgver() { | |
| 21 | + | cd neferafk | |
| 22 | + | # v0.2.0-5-gabc1234 -> 0.2.0.r5.gabc1234. Pre-release tags (v0.3.0-rc1) | |
| 23 | + | # are skipped: pacman sorts 0.3.0.rc1 after 0.3.0. | |
| 24 | + | git describe --long --tags --abbrev=7 --exclude='*-*' | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | prepare() { | |
| 28 | + | cd neferafk | |
| 29 | + | export GOPATH="$srcdir/gopath" GOFLAGS='-mod=readonly -modcacherw' GOTOOLCHAIN=local | |
| 30 | + | go mod download | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | build() { | |
| 34 | + | cd neferafk | |
| 35 | + | export GOPATH="$srcdir/gopath" CGO_ENABLED=0 GOTOOLCHAIN=local GOPROXY=off | |
| 36 | + | export GOFLAGS='-mod=readonly -modcacherw -trimpath -buildvcs=false -buildmode=pie' | |
| 37 | + | go build -ldflags "-X main.version=$pkgver" -o neferafk ./cmd/neferafk | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | check() { | |
| 41 | + | cd neferafk | |
| 42 | + | ./neferafk version | |
| 43 | + | ./neferafk validate-config examples/config | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | package() { | |
| 47 | + | cd neferafk | |
| 48 | + | install -Dm755 neferafk "$pkgdir/usr/bin/neferafk" | |
| 49 | + | install -Dm644 packaging/neferafk.pam "$pkgdir/etc/pam.d/neferafk" | |
| 50 | + | install -Dm644 examples/config "$pkgdir/usr/share/doc/neferafk/config.example" | |
| 51 | + | install -Dm644 README.md "$pkgdir/usr/share/doc/neferafk/README.md" | |
| 52 | + | install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" | |
| 53 | + | } | |
| @@ -0,0 +1,53 @@ | |||||
| 1 | + | # Maintainer: bnema <b at bnema dot dev> | |||
| 2 | + | pkgname=neferafk-git | |||
| 3 | + | pkgver=0.1.0.r0.g07cdc18 | |||
| 4 | + | pkgrel=1 | |||
| 5 | + | pkgdesc='Idle daemon for Wayland: fade, session lock, screens off and suspend (git version)' | |||
| 6 | + | arch=('x86_64' 'aarch64') | |||
| 7 | + | url='https://github.com/bnema/neferafk' | |||
| 8 | + | license=('GPL-3.0-only') | |||
| 9 | + | # Loaded at runtime (purego, no cgo): not seen by makepkg's library scan. | |||
| 10 | + | depends=('glibc' 'pam' 'libxkbcommon' 'vulkan-icd-loader' 'vulkan-driver' 'systemd' | |||
| 11 | + | 'ttf-dejavu') # lock screen monospace font (DejaVu Sans Mono) | |||
| 12 | + | makedepends=('go>=2:1.27' 'git') | |||
| 13 | + | optdepends=('pass: PIN source for auth.mode = pin') | |||
| 14 | + | provides=('neferafk') | |||
| 15 | + | conflicts=('neferafk') | |||
| 16 | + | backup=('etc/pam.d/neferafk') | |||
| 17 | + | source=('git+https://github.com/bnema/neferafk.git') | |||
| 18 | + | sha256sums=('SKIP') | |||
| 19 | + | ||||
| 20 | + | pkgver() { | |||
| 21 | + | cd neferafk | |||
| 22 | + | # v0.2.0-5-gabc1234 -> 0.2.0.r5.gabc1234. Pre-release tags (v0.3.0-rc1) | |||
| 23 | + | # are skipped: pacman sorts 0.3.0.rc1 after 0.3.0. | |||
| 24 | + | git describe --long --tags --abbrev=7 --exclude='*-*' | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | prepare() { | |||
| 28 | + | cd neferafk | |||
| 29 | + | export GOPATH="$srcdir/gopath" GOFLAGS='-mod=readonly -modcacherw' GOTOOLCHAIN=local | |||
| 30 | + | go mod download | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | build() { | |||
| 34 | + | cd neferafk | |||
| 35 | + | export GOPATH="$srcdir/gopath" CGO_ENABLED=0 GOTOOLCHAIN=local GOPROXY=off | |||
| 36 | + | export GOFLAGS='-mod=readonly -modcacherw -trimpath -buildvcs=false -buildmode=pie' | |||
| 37 | + | go build -ldflags "-X main.version=$pkgver" -o neferafk ./cmd/neferafk | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | check() { | |||
| 41 | + | cd neferafk | |||
| 42 | + | ./neferafk version | |||
| 43 | + | ./neferafk validate-config examples/config | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | package() { | |||
| 47 | + | cd neferafk | |||
| 48 | + | install -Dm755 neferafk "$pkgdir/usr/bin/neferafk" | |||
| 49 | + | install -Dm644 packaging/neferafk.pam "$pkgdir/etc/pam.d/neferafk" | |||
| 50 | + | install -Dm644 examples/config "$pkgdir/usr/share/doc/neferafk/config.example" | |||
| 51 | + | install -Dm644 README.md "$pkgdir/usr/share/doc/neferafk/README.md" | |||
| 52 | + | install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" | |||
| 53 | + | } | |||
A packaging/neferafk.pam +6 -0
| @@ -0,0 +1,6 @@ | |||
| 1 | + | # | |
| 2 | + | # PAM configuration file for the NeferAFK lock screen. By default, it includes | |
| 3 | + | # the 'login' configuration file (see /etc/pam.d/login). | |
| 4 | + | # | |
| 5 | + | ||
| 6 | + | auth include login | |
| @@ -0,0 +1,6 @@ | |||||
| 1 | + | # | |||
| 2 | + | # PAM configuration file for the NeferAFK lock screen. By default, it includes | |||
| 3 | + | # the 'login' configuration file (see /etc/pam.d/login). | |||
| 4 | + | # | |||
| 5 | + | ||||
| 6 | + | auth include login | |||