mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 09:00:35 +02:00
Ship KDE Connect inside Frame Control instead of downloading it on the Frame
The keyboard and trackpad no longer fetch KDE Connect from Valve's package repository on the Frame. The desktop apps and the iPhone app's Frame bundle carry Valve's arm64 build of kdeconnect 24.02.2-1 and the five libraries it links (kcontacts, kpeople, modemmanager-qt, pulseaudio-qt, libfakekey), pinned by SHA-256 in frame/kdeconnect/packages.json and downloaded at build time from the kdeconnect-frame-24.02.2-1 release, which also holds Valve's complete source package for each. On first use the computer copies them over its SSH connection (the iPhone bundle already has them on the Frame); the agent checks each SHA-256, unpacks them and stamps which build it is, so later starts copy nothing. No internet on the Frame, 3.6 MB instead of 8 MB, 18 MB unpacked instead of 82 MB (ModemManager and friends were packaging-only dependencies). GPL/LGPL compliance: frame/kdeconnect/NOTICE.md names each exact version, licence and source; per-project licence texts in frame/kdeconnect/LICENSES; THIRD_PARTY_NOTICES.md; an About and licences dialog in the app. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
6217b5f2fa
commit
5bf1268c82
50 files changed
+7301
-78
No files matched your search
+156
-2
@@ -71,6 +71,8 @@ class InputEvents(unittest.TestCase):
|
||||
del os.environ["FRAME_CLIENT"]
|
||||
self.assertTrue(cmd.startswith("python3 -u -c '"))
|
||||
self.assertIn(" test-client-1 ", cmd)
|
||||
cmd = self.server.InputAgent(packages=[("a.pkg.tar.zst", "ab")]).command("~/in/x")
|
||||
self.assertTrue(cmd.endswith(""" '~/in/x' '[["a.pkg.tar.zst","ab"]]'"""), cmd)
|
||||
|
||||
def test_batch_limits(self):
|
||||
with self.assertRaises(self.server.Failure):
|
||||
@@ -79,6 +81,156 @@ class InputEvents(unittest.TestCase):
|
||||
self.server.remote_input({"events": [{"dx": 1}] * (self.server.INPUT_BATCH_LIMIT + 1)})
|
||||
|
||||
|
||||
class Bundled(unittest.TestCase):
|
||||
"""KDE Connect ships with Frame Control: the manifest, the notice, the copy to the Frame."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
import server
|
||||
cls.server = server
|
||||
cls.manifest = json.loads((ROOT / "frame/kdeconnect/packages.json").read_text())
|
||||
|
||||
def test_manifest_pins_every_package(self):
|
||||
packages = self.manifest["packages"]
|
||||
self.assertEqual({p["name"] for p in packages},
|
||||
{"kdeconnect", "kcontacts", "kpeople", "libfakekey", "modemmanager-qt", "pulseaudio-qt"})
|
||||
for p in packages:
|
||||
self.assertRegex(p["sha256"], r"^[0-9a-f]{64}$")
|
||||
self.assertTrue(p["file"].startswith(f"{p['name']}-{p['version']}-") and p["file"].endswith("-aarch64.pkg.tar.zst"), p)
|
||||
self.assertTrue(p["source"].startswith(self.manifest["release"]), p)
|
||||
self.assertRegex(p["source_sha256"], r"^[0-9a-f]{64}$")
|
||||
self.assertTrue(self.manifest["release"].startswith("https://github.com/") and self.manifest["release"].endswith("/"))
|
||||
self.assertNotIn("DO_NOT_SHARE", json.dumps(self.manifest))
|
||||
|
||||
def test_notice_names_each_version_and_its_licence_texts_ship(self):
|
||||
notice = (ROOT / "frame/kdeconnect/NOTICE.md").read_text()
|
||||
for p in self.manifest["packages"]:
|
||||
self.assertIn(f"{p['name']} {p['version']}", notice)
|
||||
self.assertIn(p["source"], notice)
|
||||
self.assertIn(p["upstream"], notice)
|
||||
for spdx in p["licenses"]:
|
||||
self.assertTrue((ROOT / "frame/kdeconnect/LICENSES" / p["name"] / f"{spdx}.txt").is_file(), spdx)
|
||||
self.assertIn("frame/kdeconnect/NOTICE.md", (ROOT / "THIRD_PARTY_NOTICES.md").read_text())
|
||||
|
||||
def test_about_dialog_has_the_licences(self):
|
||||
titles = [n["title"] for n in self.server.licenses()]
|
||||
self.assertIn("Frame Control (MIT)", titles)
|
||||
self.assertIn("KDE Connect for the Frame", titles)
|
||||
self.assertIn("kdeconnect: GPL-2.0-only", titles)
|
||||
|
||||
def test_both_apps_bundle_the_packages(self):
|
||||
pkg = json.loads((ROOT / "app/package.json").read_text())["build"]["extraResources"]
|
||||
kde = next(r for r in pkg if r["from"] == "../frame/kdeconnect")
|
||||
self.assertIn("packages/*.pkg.tar.zst", kde["filter"])
|
||||
self.assertIn("LICENSES/**/*", kde["filter"])
|
||||
bundle = (ROOT / "ios/scripts/make_frame_bundle.py").read_text()
|
||||
for pattern in ("frame/kdeconnect/packages/*.pkg.tar.zst", "frame/kdeconnect/LICENSES/*/*", "THIRD_PARTY_NOTICES.md"):
|
||||
self.assertIn(pattern, bundle)
|
||||
|
||||
def fake_bundle(self, damaged=False):
|
||||
folder = Path(tempfile.mkdtemp())
|
||||
self.addCleanup(shutil.rmtree, folder)
|
||||
(folder / "packages").mkdir()
|
||||
data = {"a-1-1-aarch64.pkg.tar.zst": b"first", "b-2-1-aarch64.pkg.tar.zst": b"second"}
|
||||
packages = []
|
||||
for name, body in data.items():
|
||||
(folder / "packages" / name).write_bytes(b"x" + body if damaged else body)
|
||||
packages.append((name, __import__("hashlib").sha256(body).hexdigest()))
|
||||
return folder, packages
|
||||
|
||||
def deliver(self, frame_has, damaged=False):
|
||||
folder, packages = self.fake_bundle(damaged)
|
||||
calls = []
|
||||
|
||||
def ssh(remote, stdin=None, timeout=30, text=True):
|
||||
calls.append((remote, stdin))
|
||||
return "yes\n" if remote.startswith("{ test -x") and frame_has else ""
|
||||
old = self.server.ssh, self.server.KDECONNECT, self.server.LOCAL
|
||||
self.server.ssh, self.server.KDECONNECT, self.server.LOCAL = ssh, folder, False
|
||||
try:
|
||||
agent = self.server.InputAgent(packages=packages)
|
||||
return agent.deliver(lambda m: calls.append(("report", m))), calls, packages
|
||||
finally:
|
||||
self.server.ssh, self.server.KDECONNECT, self.server.LOCAL = old
|
||||
|
||||
def test_copies_nothing_when_the_frame_has_them(self):
|
||||
folder, calls, packages = self.deliver(frame_has=True)
|
||||
self.assertEqual(folder, "")
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0][1], "".join(f"{sha} {name}\n" for name, sha in packages))
|
||||
|
||||
def test_copies_each_package_over_ssh(self):
|
||||
folder, calls, packages = self.deliver(frame_has=False)
|
||||
self.assertTrue(folder.startswith("~/.local/share/frame-control/kdeconnect/incoming/"))
|
||||
copies = [c for c in calls if c[0] != "report" and "cat >" in c[0]]
|
||||
self.assertEqual([c[1] for c in copies], [b"first", b"second"])
|
||||
self.assertIn("a-1-1-aarch64.pkg.tar.zst.part", copies[0][0])
|
||||
|
||||
def test_refuses_a_damaged_bundle(self):
|
||||
with self.assertRaises(self.server.Failure):
|
||||
self.deliver(frame_has=False, damaged=True)
|
||||
|
||||
|
||||
@unittest.skipIf(sys.platform == "win32", "the agent runs on the Frame (Linux)")
|
||||
class AgentInstall(unittest.TestCase):
|
||||
"""The agent unpacks what the server copied, after checking each SHA-256."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
import frame_input_agent
|
||||
cls.agent = frame_input_agent
|
||||
|
||||
def setUp(self):
|
||||
self.dir = Path(tempfile.mkdtemp())
|
||||
self.addCleanup(shutil.rmtree, self.dir)
|
||||
saved = self.agent.BASE, self.agent.ROOT, self.agent.stop_daemon, self.agent.say
|
||||
self.addCleanup(lambda: setattr_all(self.agent, saved))
|
||||
self.agent.BASE, self.agent.ROOT = self.dir / "base", self.dir / "base/root"
|
||||
self.agent.stop_daemon, self.agent.say = lambda: None, lambda *a, **k: None
|
||||
self.agent.BASE.mkdir()
|
||||
|
||||
def package(self):
|
||||
src = self.dir / "src"
|
||||
(src / "usr/lib").mkdir(parents=True)
|
||||
(src / "usr/lib/kdeconnectd").write_text("#!/bin/sh\n")
|
||||
out = self.dir / "incoming/kdeconnect-24.02.2-1-aarch64.pkg.tar.zst"
|
||||
out.parent.mkdir()
|
||||
if subprocess.run(["tar", "--zstd", "-cf", str(out), "-C", str(src), "usr"], capture_output=True).returncode:
|
||||
self.skipTest("this tar can't write zstd")
|
||||
return out, [(out.name, self.agent.sha256(out))]
|
||||
|
||||
def test_unpacks_and_stamps(self):
|
||||
path, packages = self.package()
|
||||
self.assertFalse(self.agent.installed(packages))
|
||||
self.agent.install(path.parent, packages)
|
||||
self.assertTrue((self.agent.ROOT / "usr/lib/kdeconnectd").is_file())
|
||||
self.assertTrue(self.agent.installed(packages))
|
||||
self.assertFalse(self.agent.installed([(path.name, "0" * 64)]))
|
||||
|
||||
def test_refuses_a_damaged_package(self):
|
||||
path, packages = self.package()
|
||||
with open(path, "ab") as f:
|
||||
f.write(b"!")
|
||||
with self.assertRaisesRegex(RuntimeError, "damaged"):
|
||||
self.agent.install(path.parent, packages)
|
||||
self.assertFalse(self.agent.ROOT.exists())
|
||||
|
||||
def test_missing_package_and_empty_manifest(self):
|
||||
with self.assertRaisesRegex(RuntimeError, "didn't reach"):
|
||||
self.agent.install(self.dir, [("nope.pkg.tar.zst", "0" * 64)])
|
||||
with self.assertRaisesRegex(RuntimeError, "doesn't include"):
|
||||
self.agent.install(self.dir, [])
|
||||
|
||||
def test_server_and_agent_agree_on_the_stamp(self):
|
||||
import server
|
||||
packages = [("a.pkg.tar.zst", "1" * 64), ("b.pkg.tar.zst", "2" * 64)]
|
||||
self.assertEqual(server.kdeconnect_stamp(packages), self.agent.stamp(packages))
|
||||
|
||||
|
||||
def setattr_all(module, saved):
|
||||
module.BASE, module.ROOT, module.stop_daemon, module.say = saved
|
||||
|
||||
|
||||
class FakeKdeConnect:
|
||||
"""Just enough of kdeconnectd: pairs when asked and records remote-input packets."""
|
||||
|
||||
@@ -171,9 +323,11 @@ class AgentProtocol(unittest.TestCase):
|
||||
old = sys.argv
|
||||
try:
|
||||
sys.argv = ["-c", "../../etc x", "Alex's Mac"]
|
||||
self.assertEqual(self.agent.client_args(), ("etcx", "Frame Control (Alex's Mac)"))
|
||||
self.assertEqual(self.agent.client_args(), ("etcx", "Frame Control (Alex's Mac)", "", []))
|
||||
sys.argv = ["-c"]
|
||||
self.assertEqual(self.agent.client_args(), ("default", "Frame Control"))
|
||||
self.assertEqual(self.agent.client_args(), ("default", "Frame Control", "", []))
|
||||
sys.argv = ["-c", "mac", "Mac", "~/x", '[["a.pkg.tar.zst", "ab"]]']
|
||||
self.assertEqual(self.agent.client_args()[2:], (os.path.expanduser("~/x"), [("a.pkg.tar.zst", "ab")]))
|
||||
finally:
|
||||
sys.argv = old
|
||||
|
||||
|
||||
Reference in new issue
Block a user