Bug fixes and package updates.
This commit is contained in:
1 parent
19b789ef5c
commit
d0c977281d
11 files changed
+176
-27
No files matched your search
@@ -4,6 +4,7 @@ dist/
|
||||
coverage.out
|
||||
/cthulhu
|
||||
/cthulhu-braille
|
||||
/cthulhu-input-helper
|
||||
/parityledger
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
@@ -22,6 +22,7 @@ Use Go 1.24+ with CGO and development libraries listed by the Arch `PKGBUILD`.
|
||||
|
||||
- `go build -o build/cthulhu ./cmd/cthulhu`: build the reader.
|
||||
- `go build -tags brlapi -o build/cthulhu-braille ./cmd/cthulhu-braille`: build the Braille helper.
|
||||
- `go build -o build/cthulhu-input-helper ./cmd/cthulhu-input-helper`: build the remote input helper; install it beside the reader for incoming keyboard control.
|
||||
- `go test ./... -count=1`: run default tests; native cases may skip.
|
||||
- `go test -tags brlapi ./... -count=1`: run packaging checks.
|
||||
- `go vet ./...`: check common Go errors.
|
||||
|
||||
+3
-2
@@ -105,12 +105,13 @@ Tentacles already use an external process protocol: preserve existing host safeg
|
||||
|
||||
### Local installation and reload
|
||||
|
||||
After building, install both Go binaries beside one another so the reader can
|
||||
find its Braille helper:
|
||||
After building, install all three Go binaries beside one another so the reader
|
||||
can find its Braille and remote input helpers:
|
||||
|
||||
```sh
|
||||
install -Dm755 build/cthulhu "$HOME/.local/bin/cthulhu"
|
||||
install -Dm755 build/cthulhu-braille "$HOME/.local/bin/cthulhu-braille"
|
||||
install -Dm755 build/cthulhu-input-helper "$HOME/.local/bin/cthulhu-input-helper"
|
||||
"$HOME/.local/bin/cthulhu" --check-config
|
||||
```
|
||||
|
||||
|
||||
@@ -23,9 +23,10 @@ AI-assisted contributions are welcome. Contributors remain responsible for human
|
||||
|
||||
## Building
|
||||
|
||||
Run `./build-cthulhu.sh` to build `cthulhu` and `cthulhu-braille` in the
|
||||
checkout root. The script works from any directory. It uses Go's automatic
|
||||
toolchain selection with Go 1.21 or newer. If Go is missing or older, it
|
||||
Run `./build-cthulhu.sh` to build `cthulhu`, `cthulhu-braille`, and
|
||||
`cthulhu-input-helper` in the checkout root. The script works from any
|
||||
directory. It uses Go's automatic toolchain selection with Go 1.21 or newer.
|
||||
If Go is missing or older, it
|
||||
downloads the version required by `go.mod`, verifies the archive's SHA-256,
|
||||
and caches it under `${XDG_CACHE_HOME:-$HOME/.cache}/cthulhu/toolchains`.
|
||||
It does not replace system Go or require root.
|
||||
@@ -38,7 +39,7 @@ Bootstrapping Go additionally needs `curl`, `tar`, and `sha256sum`.
|
||||
See [Contributing](CONTRIBUTING.md#development-and-architecture) and the
|
||||
Arch package recipes under `distro-packages/Arch-Linux/` for dependencies.
|
||||
|
||||
For packaging, put both binaries in a separate directory and pass build flags
|
||||
For packaging, put all three binaries in a separate directory and pass build flags
|
||||
after `--`:
|
||||
|
||||
```sh
|
||||
@@ -58,6 +59,7 @@ that must use an installed toolchain:
|
||||
GOTOOLCHAIN=local GOPROXY=off ./build-cthulhu.sh --output-dir build -- -mod=readonly
|
||||
install -Dm755 build/cthulhu "$pkgdir/usr/bin/cthulhu"
|
||||
install -Dm755 build/cthulhu-braille "$pkgdir/usr/bin/cthulhu-braille"
|
||||
install -Dm755 build/cthulhu-input-helper "$pkgdir/usr/bin/cthulhu-input-helper"
|
||||
```
|
||||
|
||||
For offline builds, provision a suitable Go version and module cache in
|
||||
@@ -65,6 +67,12 @@ advance. Build inside the target distribution to link against its native
|
||||
libraries; downloading Go does not supply those libraries. `--help` lists
|
||||
the script's options without checking dependencies or accessing the network.
|
||||
|
||||
Keep all three executables together when installing or using a custom launch
|
||||
script. `cthulhu-input-helper` receives remote keyboard input on Xorg. Without
|
||||
it, Cthulhu can control another computer, but cannot accept its keyboard input.
|
||||
The October 2026 release predates this helper and incoming Remote keyboard
|
||||
support; packaging that release cannot enable the newer feature.
|
||||
|
||||
## Start with the laptop keyboard layout
|
||||
|
||||
Run `cthulhu -l` to use the laptop layout with Caps Lock as the Cthulhu
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build both Cthulhu binaries without replacing the system Go installation.
|
||||
# Build Cthulhu and its two helpers without replacing the system Go installation.
|
||||
set -euo pipefail
|
||||
|
||||
projectDir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
@@ -15,11 +15,11 @@ Usage: build-cthulhu.sh [-o DIR | --output-dir DIR] [-- GO_BUILD_FLAGS...]
|
||||
|
||||
Build cthulhu, cthulhu-braille and cthulhu-input-helper, obtaining a suitable Go toolchain if needed.
|
||||
|
||||
-o, --output-dir DIR Write both binaries to DIR (default: checkout root).
|
||||
-o, --output-dir DIR Write all three binaries to DIR (default: checkout root).
|
||||
Relative paths start at the caller's directory.
|
||||
Missing directories are created.
|
||||
-h, --help Show help without checking build dependencies.
|
||||
-- Pass remaining flags to both go build commands.
|
||||
-- Pass remaining flags to all three go build commands.
|
||||
|
||||
GOTOOLCHAIN defaults to auto. Set GOTOOLCHAIN=local to require installed Go
|
||||
and prevent toolchain downloads. Other Go settings, compiler/linker flags,
|
||||
|
||||
@@ -5,7 +5,7 @@ pkgbase=cthulhu-git
|
||||
pkgname=cthulhu-git
|
||||
_pkgname=cthulhu
|
||||
pkgver=2026.10.31.r588.g9680b58
|
||||
pkgrel=1
|
||||
pkgrel=2
|
||||
pkgdesc="Desktop-agnostic screen reader with speech, Braille, and Tentacle support"
|
||||
url="https://git.stormux.org/storm/cthulhu"
|
||||
arch=(x86_64 aarch64 armv7h)
|
||||
@@ -84,7 +84,10 @@ build() {
|
||||
|
||||
go build -o build/cthulhu ./cmd/cthulhu
|
||||
go build -tags brlapi -o build/cthulhu-braille ./cmd/cthulhu-braille
|
||||
go build -o build/cthulhu-input-helper ./cmd/cthulhu-input-helper
|
||||
# Include the helper once incoming Remote keyboard support reaches master.
|
||||
if [[ -d cmd/cthulhu-input-helper ]]; then
|
||||
go build -o build/cthulhu-input-helper ./cmd/cthulhu-input-helper
|
||||
fi
|
||||
}
|
||||
|
||||
check() {
|
||||
@@ -99,7 +102,9 @@ package() {
|
||||
cd "${_pkgname}"
|
||||
install -Dm755 build/cthulhu "${pkgdir}/usr/bin/cthulhu"
|
||||
install -Dm755 build/cthulhu-braille "${pkgdir}/usr/bin/cthulhu-braille"
|
||||
install -Dm755 build/cthulhu-input-helper "${pkgdir}/usr/bin/cthulhu-input-helper"
|
||||
if [[ -d cmd/cthulhu-input-helper ]]; then
|
||||
install -Dm755 build/cthulhu-input-helper "${pkgdir}/usr/bin/cthulhu-input-helper"
|
||||
fi
|
||||
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
install -Dm644 AUTHORS "${pkgdir}/usr/share/doc/${pkgname}/AUTHORS"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pkgbase = cthulhu
|
||||
pkgdesc = Desktop-agnostic screen reader with speech, Braille, and Tentacle support
|
||||
pkgver = 2026.10.31
|
||||
pkgrel = 3
|
||||
pkgrel = 4
|
||||
url = https://git.stormux.org/storm/cthulhu
|
||||
arch = x86_64
|
||||
arch = aarch64
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
pkgname=cthulhu
|
||||
pkgver=2026.10.31
|
||||
pkgrel=3
|
||||
pkgrel=4
|
||||
pkgdesc="Desktop-agnostic screen reader with speech, Braille, and Tentacle support"
|
||||
url="https://git.stormux.org/storm/cthulhu"
|
||||
arch=(x86_64 aarch64 armv7h)
|
||||
@@ -80,7 +80,10 @@ build() {
|
||||
|
||||
go build -o build/cthulhu ./cmd/cthulhu
|
||||
go build -tags brlapi -o build/cthulhu-braille ./cmd/cthulhu-braille
|
||||
go build -o build/cthulhu-input-helper ./cmd/cthulhu-input-helper
|
||||
# Older release tags predate incoming Remote keyboard support.
|
||||
if [[ -d cmd/cthulhu-input-helper ]]; then
|
||||
go build -o build/cthulhu-input-helper ./cmd/cthulhu-input-helper
|
||||
fi
|
||||
}
|
||||
|
||||
check() {
|
||||
@@ -95,7 +98,9 @@ package() {
|
||||
cd "${pkgname}"
|
||||
install -Dm755 build/cthulhu "${pkgdir}/usr/bin/cthulhu"
|
||||
install -Dm755 build/cthulhu-braille "${pkgdir}/usr/bin/cthulhu-braille"
|
||||
install -Dm755 build/cthulhu-input-helper "${pkgdir}/usr/bin/cthulhu-input-helper"
|
||||
if [[ -d cmd/cthulhu-input-helper ]]; then
|
||||
install -Dm755 build/cthulhu-input-helper "${pkgdir}/usr/bin/cthulhu-input-helper"
|
||||
fi
|
||||
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
install -Dm644 AUTHORS "${pkgdir}/usr/share/doc/${pkgname}/AUTHORS"
|
||||
}
|
||||
|
||||
@@ -34,13 +34,18 @@ import (
|
||||
// This uses the source-built external executable and real host/SDK IPC. Only
|
||||
// OpenLocal submissions are synthetic; it cannot prove physical-source input.
|
||||
func remote_command_fixture(t *testing.T, setup ...func(*tentacleRuntime)) (*tentacleRuntime, *consentTestForms, input.Command) {
|
||||
t.Helper()
|
||||
return remote_command_fixture_grants(t, []string{"output-observe", "remote-present"}, setup...)
|
||||
}
|
||||
|
||||
func remote_command_fixture_grants(t *testing.T, grants []string, setup ...func(*tentacleRuntime)) (*tentacleRuntime, *consentTestForms, input.Command) {
|
||||
t.Helper()
|
||||
program := os.Getenv("CTHULHU_REMOTE_BINARY")
|
||||
if program == "" {
|
||||
t.Skip("requires the separately built Remote executable")
|
||||
}
|
||||
directory := t.TempDir()
|
||||
spec := tentacle.Spec{ID: "remote", Program: []string{program, "--data-dir", filepath.Join(directory, "state")}, Presentation: true, Interaction: true, SessionEvents: true, RemoteGrants: []string{"output-observe", "remote-present"}}
|
||||
spec := tentacle.Spec{ID: "remote", Program: []string{program, "--data-dir", filepath.Join(directory, "state")}, Presentation: true, Interaction: true, SessionEvents: true, RemoteGrants: grants}
|
||||
data, _ := json.Marshal([]tentacle.Spec{spec})
|
||||
launchPath := filepath.Join(directory, "launch.json")
|
||||
if err := os.WriteFile(launchPath, data, 0600); err != nil {
|
||||
@@ -130,7 +135,7 @@ func select_remote_relay(t *testing.T, forms *consentTestForms, address, role st
|
||||
return options
|
||||
}
|
||||
|
||||
func approve_remote_form(t *testing.T, forms *consentTestForms, consent consentFormRequest, role string) consentFormRequest {
|
||||
func approve_remote_form(t *testing.T, forms *consentTestForms, consent consentFormRequest, role string, extra ...string) consentFormRequest {
|
||||
t.Helper()
|
||||
if !strings.Contains(consent.form.Title, "Remote access for remote") {
|
||||
t.Fatal("missing reader approval")
|
||||
@@ -144,7 +149,11 @@ func approve_remote_form(t *testing.T, forms *consentTestForms, consent consentF
|
||||
if role == "controlled" {
|
||||
grant = "output-observe"
|
||||
}
|
||||
consent.reply <- internalUIResult{Activated: "approve", Values: map[string]interaction.Value{grant: {Checked: true}}}
|
||||
values := map[string]interaction.Value{grant: {Checked: true}}
|
||||
for _, name := range extra {
|
||||
values[name] = interaction.Value{Checked: true}
|
||||
}
|
||||
consent.reply <- internalUIResult{Activated: "approve", Values: values}
|
||||
progress := next_remote_form(t, forms)
|
||||
if progress.form.Title != "Connecting Remote" {
|
||||
t.Fatal("approval did not start connection", progress.form.Title)
|
||||
@@ -289,8 +298,10 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
{"controller", "disconnect", true}, {"controller", "lock", true},
|
||||
{"controller", "handoff", true}, {"controller", "disable", true},
|
||||
{"controller", "backend-failure", true},
|
||||
{"controlled", "missing-helper", false},
|
||||
} {
|
||||
role, ending := test.role, test.ending
|
||||
keyboard := role == "controlled" && (ending == "disconnect" || ending == "missing-helper")
|
||||
name := role + "/" + ending
|
||||
if test.playback {
|
||||
name += "/production-speech"
|
||||
@@ -366,6 +377,18 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
}
|
||||
io.WriteString(connection, `{"type":"channel_joined","channel":"synthetic-fixture","clients":[{"id":7,"connection_type":"`+peerRole+`"}]}`+"\n")
|
||||
close(joined)
|
||||
if keyboard {
|
||||
select {
|
||||
case <-speechReady:
|
||||
case <-t.Context().Done():
|
||||
return
|
||||
}
|
||||
// NVDA and Cthulhu controllers use these same key frames.
|
||||
for _, pressed := range []bool{true, false} {
|
||||
json.NewEncoder(connection).Encode(map[string]any{"type": "key", "origin": 7,
|
||||
"vk_code": 65, "extended": false, "pressed": pressed})
|
||||
}
|
||||
}
|
||||
if role == "controller" {
|
||||
select {
|
||||
case <-speechReady:
|
||||
@@ -409,7 +432,18 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
runtime, forms, shortcut := remote_command_fixture(t, configure...)
|
||||
grants := []string{"output-observe", "remote-present"}
|
||||
var keyLog string
|
||||
if keyboard {
|
||||
grants = append(grants, "keyboard-inject")
|
||||
helper, log := keyboard_owner_helper(t)
|
||||
keyLog = log
|
||||
if ending == "missing-helper" {
|
||||
helper = filepath.Join(t.TempDir(), "absent")
|
||||
}
|
||||
configure = append(configure, func(runtime *tentacleRuntime) { runtime.remoteInputHelper = helper })
|
||||
}
|
||||
runtime, forms, shortcut := remote_command_fixture_grants(t, grants, configure...)
|
||||
if role == "controlled" {
|
||||
worker := runtime.output.speech
|
||||
worker.speaker = &observationSpeaker{fakeSpeaker{spoken: make(chan string, 64)}}
|
||||
@@ -459,7 +493,11 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
credentials.reply <- internalUIResult{Activated: "connect", Values: map[string]interaction.Value{"channel-key": {Text: "synthetic-fixture"}}}
|
||||
next_remote_form(t, forms) // Verified certificate progress.
|
||||
consent := next_remote_form(t, forms)
|
||||
status := approve_remote_form(t, forms, consent, role)
|
||||
var extra []string
|
||||
if keyboard {
|
||||
extra = append(extra, "keyboard-inject")
|
||||
}
|
||||
status := approve_remote_form(t, forms, consent, role, extra...)
|
||||
select {
|
||||
case <-joined:
|
||||
case <-time.After(5 * time.Second):
|
||||
@@ -507,6 +545,20 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
}
|
||||
}
|
||||
close(speechReady)
|
||||
if ending == "missing-helper" {
|
||||
remote_stream_wait(t, closed)
|
||||
if _, err := os.Stat(keyLog); !os.IsNotExist(err) {
|
||||
t.Fatal("missing helper unexpectedly delivered input", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if keyboard {
|
||||
fixture := &keyboardOwnerFixture{log: keyLog}
|
||||
fixture.wait(t, func() bool {
|
||||
effects := fixture.effects()
|
||||
return strings.Contains(effects, " a true\n") && strings.Contains(effects, " a false\n")
|
||||
})
|
||||
}
|
||||
if ending == "backend-failure" {
|
||||
remote_stream_wait(t, closed)
|
||||
// Backend failure retires the lease, while the process and
|
||||
|
||||
@@ -42,7 +42,7 @@ elif tool in ("go", "bootstrap-go"):
|
||||
version = "1.24.0" if tool == "bootstrap-go" else os.environ["INSTALLED_GO"]
|
||||
print("go version go" + version + " linux/amd64")
|
||||
elif args[0] == "build":
|
||||
if args[-1].endswith("cthulhu-braille") and os.environ.get("FAIL_BRAILLE"):
|
||||
if args[-1] == os.environ.get("FAIL_BUILD_TARGET"):
|
||||
sys.exit(2)
|
||||
Path(args[args.index("-o") + 1]).touch()
|
||||
'''
|
||||
@@ -162,11 +162,13 @@ class BuildScriptTests(unittest.TestCase):
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertFalse(self.calls("curl"))
|
||||
|
||||
def test_braille_failure_is_not_reported_as_success(self):
|
||||
self.env["FAIL_BRAILLE"] = "1"
|
||||
result = self.run_build()
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertNotIn("Cthulhu has been built", result.stdout)
|
||||
def test_helper_failure_is_not_reported_as_success(self):
|
||||
for helper in ("cthulhu-braille", "cthulhu-input-helper"):
|
||||
with self.subTest(helper=helper):
|
||||
self.env["FAIL_BUILD_TARGET"] = f"./cmd/{helper}"
|
||||
result = self.run_build()
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertNotIn("Cthulhu has been built", result.stdout)
|
||||
|
||||
def test_disabled_cgo_and_output_conflicts_fail_before_building(self):
|
||||
self.env["CGO_ENABLED"] = "0"
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check package contents for sources before and after Remote input support."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
MOCK_GO = '''#!/usr/bin/env python3
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
args = sys.argv[1:]
|
||||
assert args[0] == "build"
|
||||
assert Path(args[-1]).is_dir(), "build target is absent from this source version"
|
||||
if args[-1] == os.environ.get("FAIL_BUILD_TARGET"):
|
||||
sys.exit(2)
|
||||
target = Path(args[args.index("-o") + 1])
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text("#!/bin/sh\\nexit 0\\n")
|
||||
target.chmod(0o755)
|
||||
'''
|
||||
|
||||
|
||||
class PackageBinaryTests(unittest.TestCase):
|
||||
def test_build_and_install_source_supported_helpers(self):
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
recipes = sorted((root / "distro-packages").rglob("PKGBUILD"))
|
||||
self.assertTrue(recipes)
|
||||
for recipe in recipes:
|
||||
for has_input, fail_input in ((False, False), (True, False), (True, True)):
|
||||
with self.subTest(recipe=recipe.parent.name, has_input=has_input,
|
||||
fail_input=fail_input), tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
source = work / "cthulhu"
|
||||
binaries = ["cthulhu", "cthulhu-braille"]
|
||||
if has_input:
|
||||
binaries.append("cthulhu-input-helper")
|
||||
for binary in binaries:
|
||||
(source / "cmd" / binary).mkdir(parents=True)
|
||||
for name in ("LICENSE", "AUTHORS"):
|
||||
(source / name).write_text("fixture\n")
|
||||
# A stale artifact must not enter an older source's package.
|
||||
(source / "build").mkdir()
|
||||
(source / "build" / "cthulhu-input-helper").write_text("stale")
|
||||
tools = work / "tools"
|
||||
tools.mkdir()
|
||||
go = tools / "go"
|
||||
go.write_text(MOCK_GO)
|
||||
go.chmod(0o755)
|
||||
stage = work / "package"
|
||||
environment = dict(os.environ, PATH=f"{tools}:{os.environ['PATH']}",
|
||||
srcdir=str(work), pkgdir=str(stage))
|
||||
if fail_input:
|
||||
environment["FAIL_BUILD_TARGET"] = "./cmd/cthulhu-input-helper"
|
||||
result = subprocess.run(
|
||||
["bash", "-ec", 'source "$1"\nbuild\ncd "$srcdir"\npackage',
|
||||
"package-test", str(recipe)], cwd=work, env=environment,
|
||||
capture_output=True, text=True, check=False)
|
||||
if fail_input:
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertFalse(stage.exists())
|
||||
continue
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
installed = stage / "usr/bin"
|
||||
self.assertEqual(sorted(p.name for p in installed.iterdir()), sorted(binaries))
|
||||
for binary in binaries:
|
||||
self.assertTrue(os.access(installed / binary, os.X_OK))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user