diff --git a/External/kanata b/External/kanata index c50826f15..79bd7fabb 160000 --- a/External/kanata +++ b/External/kanata @@ -1 +1 @@ -Subproject commit c50826f15329dc9a1eddd59af425ea2d9ba1f392 +Subproject commit 79bd7fabb7bbb42315864dda7e364f5eafee2630 diff --git a/Package.resolved b/Package.resolved index e10c9aaff..fde1f05f6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "5513bf21d79c2d0e2c5036d55205c5492baee3caa3246324bca559157d1e6433", + "originHash" : "151bc9ca0bff6341a612d26337cd1befe80bbd520b75047dfc62ab5db7f45d3a", "pins" : [ { "identity" : "sparkle", "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "d46d456107feacc80711b21847b82b07bd9fb46e", - "version" : "2.9.3" + "revision" : "b6496a74a087257ef5e6da1c5b29a447a60f5bd7", + "version" : "2.9.4" } }, { diff --git a/Package.swift b/Package.swift index 6f548c6ae..7f5a0fa78 100644 --- a/Package.swift +++ b/Package.swift @@ -82,7 +82,7 @@ let package = Package( ], dependencies: [ // Sparkle for automatic updates - .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.8.1"), + .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.9.4"), // Snapshot testing for visual regression tests .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.17.0"), // ArgumentParser for CLI diff --git a/Rust/KeyPathKanataHostBridge/Cargo.lock b/Rust/KeyPathKanataHostBridge/Cargo.lock index 17e8d16d6..3f95b379a 100644 --- a/Rust/KeyPathKanataHostBridge/Cargo.lock +++ b/Rust/KeyPathKanataHostBridge/Cargo.lock @@ -28,15 +28,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arboard" @@ -75,9 +75,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "backtrace" @@ -111,15 +111,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bitvec" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" dependencies = [ "funty", "radium", @@ -138,15 +138,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "byteorder" @@ -162,9 +162,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "cc" -version = "1.2.56" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", @@ -180,15 +180,15 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "clap" -version = "4.5.60" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" dependencies = [ "clap_builder", "clap_derive", @@ -196,9 +196,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstyle", "clap_lex", @@ -207,21 +207,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clipboard-win" @@ -254,7 +254,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "core-foundation", "core-graphics-types", "foreign-types", @@ -267,7 +267,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "core-foundation", "libc", ] @@ -298,9 +298,6 @@ name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "dirs" @@ -329,7 +326,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "objc2", ] @@ -375,23 +372,9 @@ dependencies = [ [[package]] name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -430,13 +413,13 @@ dependencies = [ [[package]] name = "foreign-types-macros" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -451,6 +434,30 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "gethostname" version = "1.1.0" @@ -474,14 +481,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi", - "wasip2", ] [[package]] @@ -537,9 +543,9 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "image" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -571,9 +577,9 @@ dependencies = [ [[package]] name = "inotify-sys" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" dependencies = [ "libc", ] @@ -597,33 +603,34 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] [[package]] name = "kanata" -version = "1.12.0" +version = "1.12.1-prerelease-1" dependencies = [ "anyhow", "arboard", @@ -663,7 +670,7 @@ dependencies = [ [[package]] name = "kanata-keyberon" -version = "0.1120.2" +version = "0.1121.0" dependencies = [ "arraydeque", "heapless", @@ -683,10 +690,10 @@ dependencies = [ [[package]] name = "kanata-parser" -version = "0.1120.2" +version = "0.1121.0" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags 2.13.1", "bytemuck", "kanata-keyberon", "log", @@ -701,7 +708,7 @@ dependencies = [ [[package]] name = "kanata-tcp-protocol" -version = "0.1120.2" +version = "0.1121.0" dependencies = [ "serde", "serde_derive", @@ -710,7 +717,8 @@ dependencies = [ [[package]] name = "karabiner-driverkit" -version = "0.3.1" +version = "0.4.0" +source = "git+https://github.com/malpern/driverkit.git?rev=9fcaf6fe5af0059466afcd4d6c9713fd10c0216c#9fcaf6fe5af0059466afcd4d6c9713fd10c0216c" dependencies = [ "cc", "os_info", @@ -734,15 +742,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "libc", ] @@ -764,9 +772,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "malloc_buf" @@ -779,9 +787,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" @@ -821,7 +829,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -848,9 +856,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.11" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -896,7 +904,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -904,11 +912,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.30.1" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -916,9 +924,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -962,7 +970,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "objc2", "objc2-core-graphics", "objc2-foundation", @@ -974,7 +982,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "objc2", "objc2-foundation", ] @@ -995,7 +1003,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "dispatch2", "objc2", ] @@ -1006,7 +1014,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -1039,7 +1047,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -1057,7 +1065,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "block2", "libc", "objc2", @@ -1070,7 +1078,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", ] @@ -1081,7 +1089,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -1093,7 +1101,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "block2", "objc2", "objc2-cloud-kit", @@ -1129,9 +1137,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "option-ext" @@ -1141,22 +1149,22 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-float" -version = "5.1.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" dependencies = [ "num-traits", ] [[package]] name = "os_info" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b" dependencies = [ "android_system_properties", "log", - "nix 0.30.1", + "nix 0.31.3", "objc2", "objc2-foundation", "objc2-ui-kit", @@ -1209,7 +1217,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb45b6331bbdbb54c9a29413703e892ab94f83a31e4a546c778495a91e7fbca" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", ] [[package]] @@ -1218,6 +1226,12 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pin-utils" version = "0.1.0" @@ -1230,7 +1244,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "crc32fast", "fdeflate", "flate2", @@ -1245,18 +1259,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "pxfm" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" [[package]] name = "quick-error" @@ -1266,18 +1280,18 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radium" @@ -1301,7 +1315,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", ] [[package]] @@ -1317,9 +1331,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" [[package]] name = "rustc-hash" @@ -1342,7 +1356,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", @@ -1351,9 +1365,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "scopeguard" @@ -1372,15 +1386,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -1388,29 +1402,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -1421,9 +1435,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" @@ -1447,9 +1461,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simplelog" @@ -1462,23 +1476,29 @@ dependencies = [ "time", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smawk" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" dependencies = [ "lock_api", ] @@ -1525,9 +1545,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -1587,14 +1618,14 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "tiff" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" dependencies = [ "fax", "flate2", @@ -1606,12 +1637,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", - "itoa", "libc", "num-conv", "num_threads", @@ -1623,15 +1653,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -1661,20 +1691,11 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -1685,9 +1706,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1695,22 +1716,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -1923,12 +1944,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - [[package]] name = "wyz" version = "0.5.1" @@ -1957,41 +1972,41 @@ checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zune-core" -version = "0.4.12" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" [[package]] name = "zune-jpeg" -version = "0.4.21" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core", ] diff --git a/Rust/KeyPathKanataHostBridge/Cargo.toml b/Rust/KeyPathKanataHostBridge/Cargo.toml index d25acb911..ab75f8484 100644 --- a/Rust/KeyPathKanataHostBridge/Cargo.toml +++ b/Rust/KeyPathKanataHostBridge/Cargo.toml @@ -19,8 +19,5 @@ passthru-output-spike = [ [dependencies] kanata_state_machine = { package = "kanata", path = "../../External/kanata", features = ["tcp_server"] } kanata-parser = { path = "../../External/kanata/parser" } -karabiner-driverkit = "0.3.1" +karabiner-driverkit = { git = "https://github.com/malpern/driverkit.git", rev = "9fcaf6fe5af0059466afcd4d6c9713fd10c0216c" } parking_lot = "0.12" - -[patch.crates-io] -karabiner-driverkit = { path = "../../External/kanata/driverkit" } diff --git a/Scripts/README.md b/Scripts/README.md index 2beabe5ed..ac12030e7 100644 --- a/Scripts/README.md +++ b/Scripts/README.md @@ -10,8 +10,9 @@ - `./Scripts/release-doctor.sh` — Read-only preflight for signed/notarized release-candidate or public ship builds. - `./Scripts/release-candidate.sh` — Signed/notarized post-merge manual-testing build; skips snapshots, Sparkle, and website publishing by default. - `./build.sh` — Canonical build & sign entry (root). Use `SKIP_CODESIGN=1` to bypass signing for local dev. - - Release builds now fail if Sparkle EdDSA signing cannot be produced for the update archive. - - For local-only testing, set `ALLOW_UNSIGNED_SPARKLE=1` to continue without an EdDSA signature. + - Sparkle-enabled builds use Sparkle's `generate_appcast` to sign both the + update archive and complete feed with the dedicated `keypath` identity. + - `dist/sparkle/appcast.xml` is verified before the build may continue. - `./Scripts/release.sh ` — Public distribution release flow. Run `./Scripts/release-doctor.sh --ship` first. - `./Scripts/cleanup-local-build-artifacts.sh` — Dry-run cleanup of generated `.build`/`dist`/test artifacts across local worktrees. Add `--apply` to delete. - `./test.sh` — Run the full test suite (root) @@ -37,6 +38,11 @@ current documentation or automation; see `docs/process/script-surface-audit.md` for the retention audit. - `lib/signing.sh` - Thin wrappers around codesign/notarytool/stapler with `KP_SIGN_DRY_RUN=1` for safe local runs; CI overrides hook into this. +- `lib/sparkle.sh` - Resolves the pinned Sparkle tools, verifies the `keypath` + Keychain identity against `SUPublicEDKey`, and keeps private-key environment + input on stdin rather than argv. On the release machine it can read + `KEYPATH_SPARKLE_PRIVATE_KEY` from the sops-encrypted personal secrets file; + the value is never exported to unrelated build subprocesses. - `lib/deploy-lock.sh` - Shared cross-worktree lock for scripts that mutate `/Applications/KeyPath.app`. - `lib/submodules.sh` - Initializes required submodules for build paths. Kanata is hydrated automatically by release/Rust build scripts so ordinary worktrees diff --git a/Scripts/build-and-sign.sh b/Scripts/build-and-sign.sh index 2583ca1cd..535372f73 100755 --- a/Scripts/build-and-sign.sh +++ b/Scripts/build-and-sign.sh @@ -1,7 +1,9 @@ #!/bin/bash # KeyPath Build, Sign, and Notarize Script -# Run this to create a production-ready, signed, and notarized app +# Run this to create a production-ready, signed, and notarized app. +# Set SKIP_DEPLOY=1 to leave the assembled candidate in dist/ without touching +# the currently installed app or its running keyboard service. set -euo pipefail @@ -10,6 +12,8 @@ source "$SCRIPT_DIR/lib/xcode.sh" source "$SCRIPT_DIR/lib/signing.sh" source "$SCRIPT_DIR/lib/deploy-lock.sh" source "$SCRIPT_DIR/lib/submodules.sh" +source "$SCRIPT_DIR/lib/sparkle.sh" +export KEYPATH_PROJECT_DIR="$SCRIPT_DIR/.." keypath_use_stable_xcode keypath_acquire_deploy_lock "build-and-sign ($SCRIPT_DIR/..)" "${KEYPATH_RELEASE_DEPLOY_LOCK_TIMEOUT_SECONDS:-600}" trap keypath_release_deploy_lock EXIT @@ -41,103 +45,35 @@ create_sparkle_archive() { ditto -c -k --keepParent "${APP_NAME}.app" "sparkle/${ARCHIVE_NAME}" cd .. - # Sign with EdDSA using Sparkle's sign_update tool. - # Resolve dynamically because Homebrew cask versions change frequently. - local SIGN_UPDATE="" - local SIGNATURE="" - if [ -n "${KP_SPARKLE_SIGN_CMD:-}" ]; then - # Explicit override (e.g. an unattended wrapper that signs in the GUI session). - SIGN_UPDATE="$KP_SPARKLE_SIGN_CMD" - elif command -v sign_update >/dev/null 2>&1; then - SIGN_UPDATE="$(command -v sign_update)" + local GENERATE_APPCAST + GENERATE_APPCAST="$(keypath_resolve_sparkle_tool generate_appcast)" || { + echo "❌ ERROR: Sparkle generate_appcast is unavailable." >&2 + return 1 + } + local FEED_WORK_DIR="${SPARKLE_DIR}/feed" + mkdir -p "$FEED_WORK_DIR" + ditto "$SCRIPT_DIR/../appcast.xml" "$FEED_WORK_DIR/appcast.xml" + ditto "${SPARKLE_DIR}/${ARCHIVE_NAME}" "$FEED_WORK_DIR/${ARCHIVE_NAME}" + + local APPCAST_ARGS=( + --download-url-prefix "https://github.com/malpern/KeyPath/releases/download/v${VERSION}/" + --full-release-notes-url "https://github.com/malpern/KeyPath/releases/tag/v${VERSION}" + --link "https://keypath-app.com" + --versions "$BUILD" + --maximum-versions 0 + --maximum-deltas 0 + ) + if [[ "$VERSION" == *-* ]]; then + APPCAST_ARGS+=(--channel beta) else - local CASK_VERSION="" - # `|| CASK_VERSION=""` keeps `set -euo pipefail` from aborting the whole build - # when the sparkle cask isn't installed (brew exits non-zero); fall through to - # the graceful "sign_update not found" handling below instead. - CASK_VERSION="$(brew list --cask --versions sparkle 2>/dev/null | awk '{print $2}')" || CASK_VERSION="" - for CASK_ROOT in /opt/homebrew/Caskroom/sparkle /usr/local/Caskroom/sparkle; do - if [ -n "$CASK_VERSION" ] && [ -x "$CASK_ROOT/$CASK_VERSION/bin/sign_update" ]; then - SIGN_UPDATE="$CASK_ROOT/$CASK_VERSION/bin/sign_update" - break - fi - done - - # Fallback: pick the newest installed sign_update in known cask roots. - if [ -z "$SIGN_UPDATE" ]; then - local CANDIDATE="" - for CASK_ROOT in /opt/homebrew/Caskroom/sparkle /usr/local/Caskroom/sparkle; do - CANDIDATE="$(ls -1dt "$CASK_ROOT"/*/bin/sign_update 2>/dev/null | head -n1 || true)" - if [ -n "$CANDIDATE" ] && [ -x "$CANDIDATE" ]; then - SIGN_UPDATE="$CANDIDATE" - break - fi - done - fi + APPCAST_ARGS+=(--phased-rollout-interval 86400) fi - if [ -n "$SIGN_UPDATE" ] && [ -x "$SIGN_UPDATE" ]; then - echo "🔎 Using sign_update at: $SIGN_UPDATE" - echo "🔐 Signing archive with EdDSA..." - - # sign_update typically prints: sparkle:edSignature="BASE64..." - # Normalize so SIGNATURE is just the base64 payload (not the full attribute string). - local SIGN_OUTPUT - SIGN_OUTPUT=$("$SIGN_UPDATE" "${SPARKLE_DIR}/${ARCHIVE_NAME}" 2>/dev/null || echo "") - SIGNATURE=$(echo "$SIGN_OUTPUT" | sed -n 's/.*sparkle:edSignature="\([^"]*\)".*/\1/p') - if [ -z "$SIGNATURE" ]; then - # Some versions may print just the signature string; accept that form too. - SIGNATURE=$(echo "$SIGN_OUTPUT" | tr -d '\n' | tr -d '\r') - fi - - if [ -n "$SIGNATURE" ]; then - echo "$SIGNATURE" > "${SPARKLE_DIR}/${ARCHIVE_NAME}.sig" - echo "✅ EdDSA signature generated" - else - echo "⚠️ WARNING: EdDSA signing failed - check Keychain for Sparkle key" - fi - else - echo "⚠️ WARNING: sign_update not found in PATH or Homebrew Caskroom" - echo " Install Sparkle CLI tools and ensure sign_update is available on PATH." - fi - - if [ -z "$SIGNATURE" ]; then - if [ "${ALLOW_UNSIGNED_SPARKLE:-0}" = "1" ]; then - echo "⚠️ WARNING: Continuing without Sparkle EdDSA signature (ALLOW_UNSIGNED_SPARKLE=1)" - else - echo "❌ ERROR: Missing Sparkle EdDSA signature; aborting release archive generation." >&2 - echo " Ensure sign_update is installed and your Sparkle private key is available in Keychain." >&2 - echo " Set ALLOW_UNSIGNED_SPARKLE=1 only for local testing." >&2 - return 1 - fi - fi - - # Get file size - local SIZE - SIZE=$(stat -f '%z' "${SPARKLE_DIR}/${ARCHIVE_NAME}" 2>/dev/null || stat --format='%s' "${SPARKLE_DIR}/${ARCHIVE_NAME}" 2>/dev/null || echo "0") - local PUB_DATE - PUB_DATE=$(date -R 2>/dev/null || date '+%a, %d %b %Y %T %z') - - # Generate appcast entry XML - echo "📝 Generating appcast entry..." - cat > "${SPARKLE_DIR}/${ARCHIVE_NAME}.appcast-entry.xml" < - - Version ${VERSION} - ${BUILD} - ${VERSION} - 15.0 - ${PUB_DATE} - - - https://github.com/malpern/KeyPath/releases/tag/v${VERSION} - - -EOF + echo "📝 Generating and signing complete appcast with Sparkle..." + keypath_run_generate_appcast "$GENERATE_APPCAST" "${APPCAST_ARGS[@]}" "$FEED_WORK_DIR" + keypath_verify_sparkle_feed "$FEED_WORK_DIR/appcast.xml" + ditto "$FEED_WORK_DIR/appcast.xml" "$SPARKLE_DIR/appcast.xml" + echo "✅ Sparkle archive and signed appcast verified" # Create styled DMG with background image and icon positioning local DMG_NAME="KeyPath-${VERSION}.dmg" @@ -173,13 +109,17 @@ EOF echo "" echo "✅ Sparkle archive created:" echo " 📦 Archive: ${SPARKLE_DIR}/${ARCHIVE_NAME}" - echo " 🔐 Signature: ${SPARKLE_DIR}/${ARCHIVE_NAME}.sig" - echo " 📝 Appcast entry: ${SPARKLE_DIR}/${ARCHIVE_NAME}.appcast-entry.xml" + echo " 📝 Signed appcast: ${SPARKLE_DIR}/appcast.xml" echo " 💿 DMG: ${SPARKLE_DIR}/${DMG_NAME}" } keypath_ensure_kanata_submodule "$SCRIPT_DIR/.." "$SCRIPT_DIR/verify-release-signing-contract.sh" --source +if [ "${SKIP_SPARKLE:-0}" != "1" ]; then + keypath_load_sparkle_private_key || true + keypath_verify_sparkle_signing_identity "$SCRIPT_DIR/../Sources/KeyPathApp/Info.plist" + echo "✅ Sparkle signing identity '$KEYPATH_SPARKLE_ACCOUNT' matches SUPublicEDKey" +fi echo "🦀 Building Rust artifacts in parallel (kanata, simulator, host bridge)..." ./Scripts/build-kanata.sh & @@ -525,8 +465,17 @@ else echo "✨ Ready for distribution!" - # Create Sparkle-compatible versioned archive - create_sparkle_archive +fi + +# Sparkle generation is controlled independently from notarization. Public +# releases still require notarization, while SKIP_NOTARIZE=1 + SKIP_DEPLOY=1 +# provides a safe way to exercise signing and appcast generation locally. +create_sparkle_archive + +if [ "${SKIP_DEPLOY:-0}" = "1" ]; then + echo "⏭️ Skipping deployment (SKIP_DEPLOY=1)" + echo "📍 Candidate retained at: $APP_BUNDLE" + exit 0 fi # Stop running KeyPath and kanata BEFORE replacing the app bundle. diff --git a/Scripts/build-kanata.sh b/Scripts/build-kanata.sh index b79f88e48..4358b2b86 100755 --- a/Scripts/build-kanata.sh +++ b/Scripts/build-kanata.sh @@ -62,9 +62,9 @@ KANATA_FEATURES="tcp_server" # TCC-Safe Caching Logic function calculate_source_hash() { # Generate hash based on kanata source files (excluding build artifacts). - # Includes C/C++ sources: the fork vendors the karabiner-driverkit crate - # (driverkit/c_src), and a .cpp/.hpp-only change must invalidate the cache - # or a stale engine silently ships (bit MAL-57 Layer 3). + # Includes the Cargo manifests and lockfile that pin the external + # karabiner-driverkit revision. A DriverKit source update therefore changes + # the pin and invalidates the cache without requiring a sibling checkout. # Also folds in the cargo feature set: a feature change (e.g. dropping # `cmd`, #879) alters the binary without touching any source file. cd "$KANATA_SOURCE" diff --git a/Scripts/lib/sparkle.sh b/Scripts/lib/sparkle.sh new file mode 100644 index 000000000..f686c8764 --- /dev/null +++ b/Scripts/lib/sparkle.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash + +# Shared Sparkle tooling for release scripts. This file intentionally keeps the +# private key out of argv and logs: interactive releases use the dedicated +# Keychain account, while unattended callers may pipe KEYPATH_SPARKLE_PRIVATE_KEY +# to Sparkle through stdin. + +KEYPATH_SPARKLE_ACCOUNT="${KEYPATH_SPARKLE_ACCOUNT:-keypath}" + +keypath_load_sparkle_private_key() { + if [[ -n "${KEYPATH_SPARKLE_PRIVATE_KEY:-}" ]]; then + return 0 + fi + + local secrets_file="${KEYPATH_SECRETS_FILE:-$HOME/dotfiles/secrets.env}" + if ! command -v sops >/dev/null 2>&1 || [[ ! -f "$secrets_file" ]]; then + return 1 + fi + + local line key value + while IFS= read -r line; do + key="${line%%=*}" + value="${line#*=}" + if [[ "$key" == "KEYPATH_SPARKLE_PRIVATE_KEY" ]]; then + KEYPATH_SPARKLE_PRIVATE_KEY="$value" + return 0 + fi + done < <(sops -d "$secrets_file" 2>/dev/null) + + return 1 +} + +keypath_resolve_sparkle_tool() { + local tool_name=$1 + local override_value="" + case "$tool_name" in + generate_appcast) override_value="${KP_SPARKLE_GENERATE_APPCAST_CMD:-}" ;; + generate_keys) override_value="${KP_SPARKLE_GENERATE_KEYS_CMD:-}" ;; + sign_update) override_value="${KP_SPARKLE_SIGN_CMD:-}" ;; + *) + echo "Unsupported Sparkle tool: $tool_name" >&2 + return 2 + ;; + esac + + if [[ -n "$override_value" && -x "$override_value" ]]; then + echo "$override_value" + return 0 + fi + + local project_dir="${KEYPATH_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." >/dev/null && pwd)}" + local artifact_tool="$project_dir/.build/artifacts/sparkle/Sparkle/bin/$tool_name" + if [[ -x "$artifact_tool" ]]; then + echo "$artifact_tool" + return 0 + fi + + if command -v "$tool_name" >/dev/null 2>&1; then + command -v "$tool_name" + return 0 + fi + + local cask_version="" + cask_version="$(brew list --cask --versions sparkle 2>/dev/null | awk '{print $2}')" || cask_version="" + local cask_root candidate + for cask_root in /opt/homebrew/Caskroom/sparkle /usr/local/Caskroom/sparkle; do + if [[ -n "$cask_version" && -x "$cask_root/$cask_version/bin/$tool_name" ]]; then + echo "$cask_root/$cask_version/bin/$tool_name" + return 0 + fi + candidate="$(ls -1dt "$cask_root"/*/bin/"$tool_name" 2>/dev/null | head -n1 || true)" + if [[ -n "$candidate" && -x "$candidate" ]]; then + echo "$candidate" + return 0 + fi + done + + return 1 +} + +keypath_sparkle_embedded_public_key() { + local info_plist=$1 + /usr/libexec/PlistBuddy -c "Print :SUPublicEDKey" "$info_plist" 2>/dev/null +} + +keypath_sparkle_public_key_from_private_key() { + printf '%s' "$KEYPATH_SPARKLE_PRIVATE_KEY" | xcrun swift -e ' +import CryptoKit +import Foundation + +let input = FileHandle.standardInput.readDataToEndOfFile() +guard let encoded = String(data: input, encoding: .utf8), + let seed = Data(base64Encoded: encoded.trimmingCharacters(in: .whitespacesAndNewlines)) else { + exit(2) +} +let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: seed) +print(privateKey.publicKey.rawRepresentation.base64EncodedString()) +' 2>/dev/null | tr -d '\r\n' +} + +keypath_verify_sparkle_signing_identity() { + local info_plist=$1 + local expected_public_key actual_public_key + expected_public_key="$(keypath_sparkle_embedded_public_key "$info_plist")" || { + echo "SUPublicEDKey is missing from $info_plist" >&2 + return 1 + } + if [[ -n "${KEYPATH_SPARKLE_PRIVATE_KEY:-}" ]]; then + actual_public_key="$(keypath_sparkle_public_key_from_private_key)" || { + echo "Could not derive a public key from KEYPATH_SPARKLE_PRIVATE_KEY" >&2 + return 1 + } + else + local generate_keys + generate_keys="$(keypath_resolve_sparkle_tool generate_keys)" || { + echo "Sparkle generate_keys is unavailable" >&2 + return 1 + } + actual_public_key="$("$generate_keys" --account "$KEYPATH_SPARKLE_ACCOUNT" -p 2>/dev/null | tr -d '\r\n')" || { + echo "Could not read Sparkle signing identity '$KEYPATH_SPARKLE_ACCOUNT' from Keychain" >&2 + return 1 + } + fi + + if [[ -z "$actual_public_key" || "$actual_public_key" != "$expected_public_key" ]]; then + echo "Sparkle signing identity '$KEYPATH_SPARKLE_ACCOUNT' does not match SUPublicEDKey" >&2 + return 1 + fi +} + +keypath_run_generate_appcast() { + local generate_appcast=$1 + shift + if [[ -n "${KEYPATH_SPARKLE_PRIVATE_KEY:-}" ]]; then + printf '%s' "$KEYPATH_SPARKLE_PRIVATE_KEY" | "$generate_appcast" --ed-key-file - "$@" + else + "$generate_appcast" --account "$KEYPATH_SPARKLE_ACCOUNT" "$@" + fi +} + +keypath_verify_sparkle_feed() { + local feed_path=$1 + local sign_update + sign_update="$(keypath_resolve_sparkle_tool sign_update)" || { + echo "Sparkle sign_update is unavailable" >&2 + return 1 + } + + if [[ -n "${KEYPATH_SPARKLE_PRIVATE_KEY:-}" ]]; then + printf '%s' "$KEYPATH_SPARKLE_PRIVATE_KEY" | "$sign_update" --ed-key-file - --verify "$feed_path" + else + "$sign_update" --account "$KEYPATH_SPARKLE_ACCOUNT" --verify "$feed_path" + fi +} diff --git a/Scripts/release-doctor.sh b/Scripts/release-doctor.sh index e93a71a9b..e166922e0 100755 --- a/Scripts/release-doctor.sh +++ b/Scripts/release-doctor.sh @@ -4,6 +4,8 @@ set -euo pipefail SCRIPT_DIR=$(cd "$(dirname "$0")" >/dev/null && pwd) PROJECT_DIR=$(cd "$SCRIPT_DIR/.." >/dev/null && pwd) source "$SCRIPT_DIR/lib/xcode.sh" +source "$SCRIPT_DIR/lib/sparkle.sh" +export KEYPATH_PROJECT_DIR="$PROJECT_DIR" keypath_use_stable_xcode MODE="release-candidate" @@ -102,35 +104,6 @@ notarytool() { xcrun notarytool "$@" } -resolve_sign_update() { - if [[ -n "${KP_SPARKLE_SIGN_CMD:-}" && -x "${KP_SPARKLE_SIGN_CMD:-}" ]]; then - echo "$KP_SPARKLE_SIGN_CMD" - return 0 - fi - - if command -v sign_update >/dev/null 2>&1; then - command -v sign_update - return 0 - fi - - local cask_version="" - cask_version="$(brew list --cask --versions sparkle 2>/dev/null | awk '{print $2}')" || cask_version="" - local cask_root candidate - for cask_root in /opt/homebrew/Caskroom/sparkle /usr/local/Caskroom/sparkle; do - if [[ -n "$cask_version" && -x "$cask_root/$cask_version/bin/sign_update" ]]; then - echo "$cask_root/$cask_version/bin/sign_update" - return 0 - fi - candidate="$(ls -1dt "$cask_root"/*/bin/sign_update 2>/dev/null | head -n1 || true)" - if [[ -n "$candidate" && -x "$candidate" ]]; then - echo "$candidate" - return 0 - fi - done - - return 1 -} - find_worktree_for_branch() { local branch_name=$1 git worktree list --porcelain | awk -v target="refs/heads/${branch_name}" ' @@ -254,14 +227,37 @@ fi if [[ "$effective_skip_sparkle" == "1" ]]; then pass "Sparkle archive checks skipped (SKIP_SPARKLE=1)" else - if sign_update_path=$(resolve_sign_update); then + keypath_load_sparkle_private_key || true + if sign_update_path=$(keypath_resolve_sparkle_tool sign_update); then pass "Sparkle sign_update found: $sign_update_path" - elif [[ "${ALLOW_UNSIGNED_SPARKLE:-0}" == "1" ]]; then - warn "Sparkle sign_update missing, but ALLOW_UNSIGNED_SPARKLE=1" else fail "Sparkle sign_update not found; public Sparkle archive signing would fail" fi + if generate_appcast_path=$(keypath_resolve_sparkle_tool generate_appcast); then + pass "Sparkle generate_appcast found: $generate_appcast_path" + else + fail "Sparkle generate_appcast not found; public appcast generation would fail" + fi + + if generate_keys_path=$(keypath_resolve_sparkle_tool generate_keys); then + pass "Sparkle generate_keys found: $generate_keys_path" + else + fail "Sparkle generate_keys not found; signing identity cannot be verified" + fi + + if keypath_verify_sparkle_signing_identity "$PROJECT_DIR/Sources/KeyPathApp/Info.plist"; then + pass "Sparkle signing identity '$KEYPATH_SPARKLE_ACCOUNT' matches SUPublicEDKey" + else + fail "Sparkle signing identity '$KEYPATH_SPARKLE_ACCOUNT' does not match SUPublicEDKey" + fi + + if keypath_verify_sparkle_feed "$PROJECT_DIR/appcast.xml" >/dev/null 2>&1; then + pass "Committed Sparkle appcast signature verified" + else + fail "Committed Sparkle appcast is unsigned or has an invalid signature" + fi + if command -v create-dmg >/dev/null 2>&1; then pass "create-dmg available" else diff --git a/Scripts/release.sh b/Scripts/release.sh index 43037b188..12d70016c 100755 --- a/Scripts/release.sh +++ b/Scripts/release.sh @@ -44,7 +44,7 @@ find_worktree_for_branch() { } usage() { - echo "Usage: $0 [--dry-run] [--no-doctor] [--refresh-keyboard-data] [--skip-notarize dry-run only] [X.Y.Z]" + echo "Usage: $0 [--dry-run] [--no-doctor] [--refresh-keyboard-data] [--skip-notarize dry-run only] [X.Y.Z[-prerelease]]" } # Parse arguments @@ -63,7 +63,7 @@ for arg in "$@"; do REFRESH_KEYBOARD_DATA=true ;; *) - if [[ $arg =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + if [[ $arg =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$ ]]; then NEW_VERSION="$arg" else echo "❌ Invalid argument: $arg" @@ -86,29 +86,47 @@ if [ "$SKIP_NOTARIZE" = true ] && [ "$DRY_RUN" = false ]; then exit 1 fi -# Get current version -CURRENT_VERSION=$(defaults read "$INFO_PLIST" CFBundleShortVersionString 2>/dev/null || echo "0.0.0") -echo "📌 Current version: $CURRENT_VERSION" +# Get current version and monotonically increasing Sparkle build number. +CURRENT_VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$INFO_PLIST" 2>/dev/null || echo "0.0.0") +CURRENT_BUILD=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$INFO_PLIST" 2>/dev/null || echo "0") +echo "📌 Current version: $CURRENT_VERSION (build $CURRENT_BUILD)" # Bump version if specified if [ -n "$NEW_VERSION" ]; then - echo "📝 Bumping version to: $NEW_VERSION" + RELEASE_BUILD_NUMBER="${KEYPATH_RELEASE_BUILD_NUMBER:-}" + if [ -z "$RELEASE_BUILD_NUMBER" ]; then + if [[ "$CURRENT_BUILD" =~ ^[0-9]+$ ]]; then + RELEASE_BUILD_NUMBER=$((CURRENT_BUILD + 1)) + else + echo "❌ Current CFBundleVersion is not numeric: $CURRENT_BUILD" >&2 + echo " Set KEYPATH_RELEASE_BUILD_NUMBER to the next numeric build." >&2 + exit 1 + fi + fi + if [[ ! "$RELEASE_BUILD_NUMBER" =~ ^[0-9]+$ ]]; then + echo "❌ KEYPATH_RELEASE_BUILD_NUMBER must be numeric: $RELEASE_BUILD_NUMBER" >&2 + exit 1 + fi + + echo "📝 Bumping version to: $NEW_VERSION (build $RELEASE_BUILD_NUMBER)" if [ "$DRY_RUN" = true ]; then echo " [DRY RUN] Would update Info.plist" else /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $NEW_VERSION" "$INFO_PLIST" - /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $NEW_VERSION" "$INFO_PLIST" + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $RELEASE_BUILD_NUMBER" "$INFO_PLIST" echo " ✅ Updated Info.plist" fi VERSION="$NEW_VERSION" else VERSION="$CURRENT_VERSION" + RELEASE_BUILD_NUMBER="$CURRENT_BUILD" fi echo "" echo "🎯 Release version: $VERSION" +echo "🔢 Sparkle build: $RELEASE_BUILD_NUMBER" if [ "$SKIP_NOTARIZE" = true ]; then echo "⚠️ Notarization: skipped (--skip-notarize dry-run only)" fi @@ -152,11 +170,9 @@ if [ "$DRY_RUN" = true ]; then STEP=$((STEP + 1)) echo " ${STEP}. Create Sparkle archive: dist/sparkle/KeyPath-${VERSION}.zip" STEP=$((STEP + 1)) - echo " ${STEP}. Sign Sparkle archive with EdDSA" - STEP=$((STEP + 1)) echo " ${STEP}. Create DMG: dist/sparkle/KeyPath-${VERSION}.dmg" STEP=$((STEP + 1)) - echo " ${STEP}. Generate appcast entry" + echo " ${STEP}. Generate and verify the signed Sparkle appcast" STEP=$((STEP + 1)) echo "" echo " Then you would:" @@ -164,7 +180,7 @@ if [ "$DRY_RUN" = true ]; then STEP=$((STEP + 1)) echo " ${STEP}. Create GitHub Release and upload ZIP + DMG" STEP=$((STEP + 1)) - echo " ${STEP}. Update appcast.xml with generated entry" + echo " ${STEP}. Publish the generated signed appcast.xml" STEP=$((STEP + 1)) echo " ${STEP}. git add appcast.xml && git commit -m 'chore: update appcast for v${VERSION}'" STEP=$((STEP + 1)) @@ -204,7 +220,12 @@ if [ ! -f "$SPARKLE_ZIP" ]; then fi SPARKLE_DMG="dist/sparkle/KeyPath-${VERSION}.dmg" -APPCAST_ENTRY="dist/sparkle/KeyPath-${VERSION}.zip.appcast-entry.xml" +SPARKLE_APPCAST="dist/sparkle/appcast.xml" + +if [ ! -f "$SPARKLE_APPCAST" ]; then + echo "❌ ERROR: Signed Sparkle appcast not found: $SPARKLE_APPCAST" + exit 1 +fi echo "" echo "✅ Release build complete!" @@ -213,7 +234,7 @@ echo "📦 Artifacts:" echo " • dist/KeyPath.app" echo " • $SPARKLE_ZIP" echo " • $SPARKLE_DMG" -echo " • $APPCAST_ENTRY" +echo " • $SPARKLE_APPCAST" echo "" # --- Automated release steps --- @@ -244,33 +265,10 @@ gh release create "v${VERSION}" \ --notes "See [release notes](https://github.com/malpern/KeyPath/releases/tag/v${VERSION}) for details." echo "📝 Updating appcast.xml..." -if [ -f "$APPCAST_ENTRY" ]; then - # Insert the new entry after the "Releases go here" comment - ENTRY_CONTENT=$(cat "$APPCAST_ENTRY" | grep -v "^' -if marker in appcast: - appcast = appcast.replace(marker, marker + '\n\n' + indented.rstrip()) - open('appcast.xml', 'w').write(appcast) - print(' ✅ Appcast updated') -else: - print(' ⚠️ Could not find marker in appcast.xml — update manually') - sys.exit(1) -" - git add appcast.xml - git commit -m "chore: update appcast for v${VERSION}" -else - echo " ⚠️ Appcast entry not found at $APPCAST_ENTRY — update manually" -fi +ditto "$SPARKLE_APPCAST" appcast.xml +git add appcast.xml +git commit -m "chore: update signed appcast for v${VERSION}" +echo " ✅ Signed appcast updated" echo "🌐 Updating gh-pages download link..." GHPAGES_WORKTREE=$(find_worktree_for_branch gh-pages) diff --git a/Sources/KeyPathApp/Info.plist b/Sources/KeyPathApp/Info.plist index 9c3b0d121..3540791c0 100644 --- a/Sources/KeyPathApp/Info.plist +++ b/Sources/KeyPathApp/Info.plist @@ -56,8 +56,14 @@ Jry+V9z0EbEysm5FWpbL8J3XfeFnzWqGI3Y3+CQiHgo= SUEnableAutomaticChecks + SUAutomaticallyUpdate + SUScheduledCheckInterval 86400 + SUVerifyUpdateBeforeExtraction + + SURequireSignedFeed + SUShowReleaseNotes SUEnableSystemProfiling diff --git a/Sources/KeyPathApp/Resources/Karabiner-DriverKit-VirtualHIDDevice-6.2.0.pkg b/Sources/KeyPathApp/Resources/Karabiner-DriverKit-VirtualHIDDevice-6.2.0.pkg deleted file mode 100644 index 476bb87c6..000000000 Binary files a/Sources/KeyPathApp/Resources/Karabiner-DriverKit-VirtualHIDDevice-6.2.0.pkg and /dev/null differ diff --git a/Sources/KeyPathApp/Resources/Karabiner-DriverKit-VirtualHIDDevice-8.0.0.pkg b/Sources/KeyPathApp/Resources/Karabiner-DriverKit-VirtualHIDDevice-8.0.0.pkg new file mode 100644 index 000000000..8e4fdf0be Binary files /dev/null and b/Sources/KeyPathApp/Resources/Karabiner-DriverKit-VirtualHIDDevice-8.0.0.pkg differ diff --git a/Sources/KeyPathAppKit/Services/UpdateService.swift b/Sources/KeyPathAppKit/Services/UpdateService.swift index fe3853fa4..7614c0b38 100644 --- a/Sources/KeyPathAppKit/Services/UpdateService.swift +++ b/Sources/KeyPathAppKit/Services/UpdateService.swift @@ -13,6 +13,22 @@ public enum UpdateChannel: String, CaseIterable, Identifiable { } } +/// Sparkle supplies its continuation as an Objective-C block without Sendable +/// annotations. The updater invokes it exactly once on the main actor after +/// runtime preparation, so this narrow wrapper documents that ownership +/// transfer for Swift 6 concurrency checking. +private final class SparkleInstallHandler: @unchecked Sendable { + private let block: () -> Void + + init(_ block: @escaping () -> Void) { + self.block = block + } + + func invoke() { + block() + } +} + /// Manages application updates via Sparkle framework /// /// This service handles: @@ -36,6 +52,8 @@ public final class UpdateService: NSObject { public private(set) var canCheckForUpdates = false public private(set) var lastUpdateCheckDate: Date? public private(set) var automaticallyChecksForUpdates = true + public private(set) var automaticallyDownloadsUpdates = true + public private(set) var allowsAutomaticUpdates = true public private(set) var updateChannel: UpdateChannel = .stable public private(set) var currentFeedURL: String? @@ -72,11 +90,13 @@ public final class UpdateService: NSObject { canCheckForUpdates = updater.canCheckForUpdates lastUpdateCheckDate = updater.lastUpdateCheckDate automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates + automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates + allowsAutomaticUpdates = updater.allowsAutomaticUpdates let persistedChannel = loadPersistedChannel() setUpdateChannel(persistedChannel) AppLogger.shared.log( - "✅ [UpdateService] Sparkle initialized - autoCheck: \(automaticallyChecksForUpdates), channel: \(updateChannel.rawValue)" + "✅ [UpdateService] Sparkle initialized - autoCheck: \(automaticallyChecksForUpdates), autoInstall: \(automaticallyDownloadsUpdates), channel: \(updateChannel.rawValue)" ) } } @@ -91,9 +111,27 @@ public final class UpdateService: NSObject { public func setAutomaticChecks(enabled: Bool) { updaterController?.updater.automaticallyChecksForUpdates = enabled automaticallyChecksForUpdates = enabled + allowsAutomaticUpdates = updaterController?.updater.allowsAutomaticUpdates ?? false + automaticallyDownloadsUpdates = updaterController?.updater.automaticallyDownloadsUpdates ?? false AppLogger.shared.log("⚙️ [UpdateService] Automatic checks set to: \(enabled)") } + /// Enable or disable Sparkle's automatic background download/install path. + /// This is a user preference; Info.plist supplies only the initial default. + public func setAutomaticDownloads(enabled: Bool) { + guard let updater = updaterController?.updater, updater.allowsAutomaticUpdates else { + AppLogger.shared.warn("⚠️ [UpdateService] Automatic updates are unavailable while automatic checks are disabled") + automaticallyDownloadsUpdates = false + allowsAutomaticUpdates = false + return + } + + updater.automaticallyDownloadsUpdates = enabled + automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates + allowsAutomaticUpdates = updater.allowsAutomaticUpdates + AppLogger.shared.log("⚙️ [UpdateService] Automatic download/install set to: \(automaticallyDownloadsUpdates)") + } + public func setUpdateChannel(_ channel: UpdateChannel) { updateChannel = channel UserDefaults.standard.set(channel.rawValue, forKey: channelDefaultsKey) @@ -131,27 +169,40 @@ extension UpdateService: SPUUpdaterDelegate { return channel == .beta ? ["beta"] : [] } - /// Called before an update is about to be installed - /// We use this to stop kanata and helper services before Sparkle replaces the app bundle + /// Called immediately before Sparkle begins installation. Runtime shutdown + /// is completed by `shouldPostponeRelaunchForUpdate` before Sparkle reaches + /// this callback; do not launch asynchronous preparation from here because + /// Sparkle does not wait for this method to return. public nonisolated func updater( _: SPUUpdater, willInstallUpdate item: SUAppcastItem ) { - // Extract version string before crossing isolation boundary let version = item.displayVersionString Task { @MainActor in - await prepareForUpdate(version: version) + AppLogger.shared.log("📦 [UpdateService] Installing prepared KeyPath update v\(version)") } } - /// Provide custom appcast item comparison if needed + /// Hold Sparkle at its supported pre-install boundary while KeyPath stops + /// the app-bundled Kanata runtime. The handler is invoked even when shutdown + /// cannot be proven so an update never remains permanently wedged; the + /// failure is surfaced and the normal post-relaunch validation remains the + /// authority for recovery. public nonisolated func updater( _: SPUUpdater, - shouldPostponeRelaunchForUpdate _: SUAppcastItem, - untilInvokingBlock _: @escaping () -> Void + shouldPostponeRelaunchForUpdate item: SUAppcastItem, + untilInvokingBlock installHandler: @escaping () -> Void ) -> Bool { - // Don't postpone - let Sparkle handle the relaunch timing - false + let version = item.displayVersionString + let handler = SparkleInstallHandler(installHandler) + Task { @MainActor in + defer { + AppLogger.shared.log("▶️ [UpdateService] Runtime preparation finished; releasing Sparkle installer") + handler.invoke() + } + await prepareForUpdate(version: version) + } + return true } public nonisolated func updater(_: SPUUpdater, didAbortWithError error: Error) { @@ -220,7 +271,7 @@ extension UpdateService: SPUUpdaterDelegate { extension UpdateService { enum UpdateRepairDecision: Equatable { case silentContinue(reason: String) - case automaticRepairAllowed(reason: String) + case runtimeShutdownRequired(reason: String) case userRepairRequired(reason: String) case manualAttentionRequired(reason: String) } @@ -251,12 +302,16 @@ extension UpdateService { switch Self.preUpdateDecision(for: context) { case let .silentContinue(reason): AppLogger.shared.log("✅ [UpdateService] Pre-update: no preparation needed (\(reason))") - case let .automaticRepairAllowed(reason): - AppLogger.shared.log("🔧 [UpdateService] Pre-update: running repair (\(reason))") - let report = await engine.run(intent: .repair, using: broker) - AppLogger.shared.log( - "✅ [UpdateService] Services prepared for update - success: \(report.success)" - ) + case let .runtimeShutdownRequired(reason): + AppLogger.shared.log("⏸️ [UpdateService] Pre-update: stopping bundled Kanata runtime (\(reason))") + let report = await engine.runSingleAction(.terminateConflictingProcesses, using: broker) + if report.success { + AppLogger.shared.log("✅ [UpdateService] Bundled Kanata runtime stopped before update") + } else { + AppLogger.shared.error( + "⚠️ [UpdateService] Could not prove Kanata stopped before update: \(report.failureReason ?? "unknown error")" + ) + } case let .userRepairRequired(reason), let .manualAttentionRequired(reason): AppLogger.shared.error( "⚠️ [UpdateService] Pre-update repair not allowed for decision \(reason); continuing without mutation" @@ -284,9 +339,9 @@ extension UpdateService { ) MainAppStateController.shared.invalidateValidationCooldown() await MainAppStateController.shared.revalidate() - case let .automaticRepairAllowed(reason): + case let .runtimeShutdownRequired(reason): AppLogger.shared.error( - "⚠️ [UpdateService] Post-update automatic repair is disallowed by Phase 1 W3 (\(reason)); surfacing status instead" + "⚠️ [UpdateService] Unexpected post-update shutdown decision (\(reason)); surfacing status instead" ) MainAppStateController.shared.invalidateValidationCooldown() await MainAppStateController.shared.revalidate() @@ -295,7 +350,7 @@ extension UpdateService { nonisolated static func preUpdateDecision(for context: SystemContext) -> UpdateRepairDecision { if context.services.kanataRunning || context.services.karabinerDaemonRunning || context.helper.isInstalled { - return .automaticRepairAllowed(reason: "reason_code=services_or_helper_present") + return .runtimeShutdownRequired(reason: "reason_code=services_or_helper_present") } return .silentContinue(reason: "reason_code=nothing_running") } diff --git a/Sources/KeyPathAppKit/UI/AboutView.swift b/Sources/KeyPathAppKit/UI/AboutView.swift index 301357e7c..7bf0bffa2 100644 --- a/Sources/KeyPathAppKit/UI/AboutView.swift +++ b/Sources/KeyPathAppKit/UI/AboutView.swift @@ -182,6 +182,13 @@ struct AboutView: View { )) .accessibilityIdentifier("about-auto-update-toggle") + Toggle("Download and install updates automatically", isOn: Binding( + get: { updateService.automaticallyDownloadsUpdates }, + set: { updateService.setAutomaticDownloads(enabled: $0) } + )) + .disabled(!updateService.allowsAutomaticUpdates) + .accessibilityIdentifier("about-auto-install-toggle") + HStack(alignment: .center, spacing: 12) { Text("Update Channel") .font(.subheadline.weight(.medium)) diff --git a/Sources/KeyPathCore/WizardSystemPaths.swift b/Sources/KeyPathCore/WizardSystemPaths.swift index fd0c96394..6fff80a36 100644 --- a/Sources/KeyPathCore/WizardSystemPaths.swift +++ b/Sources/KeyPathCore/WizardSystemPaths.swift @@ -273,7 +273,7 @@ public enum WizardSystemPaths { /// The bundled VHID driver version - single source of truth /// Update this when bundling a new driver version - public static let bundledVHIDDriverVersion = "6.2.0" + public static let bundledVHIDDriverVersion = "8.0.0" /// The bundled VHID driver major version (for compatibility checks) public static var bundledVHIDDriverMajorVersion: Int { diff --git a/Sources/KeyPathHelper/HelperService.swift b/Sources/KeyPathHelper/HelperService.swift index 5ecf708ee..1dad57286 100644 --- a/Sources/KeyPathHelper/HelperService.swift +++ b/Sources/KeyPathHelper/HelperService.swift @@ -930,7 +930,7 @@ extension HelperService { // IMPORTANT: Keep in sync with WizardSystemPaths.bundledVHIDDriverVersion in KeyPathCore // The helper doesn't have access to KeyPathCore, so we maintain a copy here // This is only used for the deprecated download fallback path - private static let requiredVHIDVersion = "6.2.0" + private static let requiredVHIDVersion = "8.0.0" private static func consoleUserInfo() -> (name: String, home: String)? { var uid: uid_t = 0 diff --git a/Sources/KeyPathInstallationWizard/Core/VHIDDeviceManager.swift b/Sources/KeyPathInstallationWizard/Core/VHIDDeviceManager.swift index 2c7685f84..65870114a 100644 --- a/Sources/KeyPathInstallationWizard/Core/VHIDDeviceManager.swift +++ b/Sources/KeyPathInstallationWizard/Core/VHIDDeviceManager.swift @@ -64,8 +64,8 @@ public final class VHIDDeviceManager: @unchecked Sendable { } /// Version compatibility for kanata - uses bundled driver as single source of truth - /// NOTE: Kanata v1.12.0 requires Karabiner-DriverKit-VirtualHIDDevice v6.2.0. - /// Updated July 2026 with the Kanata v1.12.0 runtime pairing. + /// NOTE: Kanata v1.12.1-prerelease-1 uses DriverKit protocol 7 and requires + /// Karabiner-DriverKit-VirtualHIDDevice v8.0.0. private static var requiredDriverVersionMajor: Int { WizardSystemPaths.bundledVHIDDriverMajorVersion } @@ -74,7 +74,7 @@ public final class VHIDDeviceManager: @unchecked Sendable { WizardSystemPaths.bundledVHIDDriverVersion } - private static let currentKanataVersion = "1.12.0" + private static let currentKanataVersion = "1.12.1-prerelease-1" // Driver DriverKit extension identifiers private static let driverTeamID = "G43BCU2T37" // pqrs.org team ID diff --git a/Tests/KeyPathTests/BuildScripts/SparkleUpdateContractTests.swift b/Tests/KeyPathTests/BuildScripts/SparkleUpdateContractTests.swift new file mode 100644 index 000000000..6cecb4e49 --- /dev/null +++ b/Tests/KeyPathTests/BuildScripts/SparkleUpdateContractTests.swift @@ -0,0 +1,97 @@ +import Foundation +@preconcurrency import XCTest + +final class SparkleUpdateContractTests: XCTestCase { + private let releasedPublicKey = "Jry+V9z0EbEysm5FWpbL8J3XfeFnzWqGI3Y3+CQiHgo=" + + func testSparkleDependencyIsPinnedToCurrentRelease() throws { + let root = repositoryRoot() + let manifest = try contents(of: root.appendingPathComponent("Package.swift")) + let resolvedData = try Data(contentsOf: root.appendingPathComponent("Package.resolved")) + let resolved = try XCTUnwrap( + JSONSerialization.jsonObject(with: resolvedData) as? [String: Any] + ) + let pins = try XCTUnwrap(resolved["pins"] as? [[String: Any]]) + let sparkle = try XCTUnwrap(pins.first { $0["identity"] as? String == "sparkle" }) + let state = try XCTUnwrap(sparkle["state"] as? [String: Any]) + + XCTAssertTrue(manifest.contains(#"from: "2.9.4""#)) + XCTAssertEqual(state["version"] as? String, "2.9.4") + } + + func testHardenedSparkleConfigurationIsEnabledWithoutProfiling() throws { + let plist = try propertyList( + at: repositoryRoot().appendingPathComponent("Sources/KeyPathApp/Info.plist") + ) + + XCTAssertEqual(plist["SUPublicEDKey"] as? String, releasedPublicKey) + XCTAssertEqual(plist["SUEnableAutomaticChecks"] as? Bool, true) + XCTAssertEqual(plist["SUAutomaticallyUpdate"] as? Bool, true) + XCTAssertEqual(plist["SUVerifyUpdateBeforeExtraction"] as? Bool, true) + XCTAssertEqual(plist["SURequireSignedFeed"] as? Bool, true) + XCTAssertEqual(plist["SUEnableSystemProfiling"] as? Bool, false) + } + + func testCommittedFeedIsSignedAndHistoricalBetaIsOnBetaChannel() throws { + let appcast = try contents(of: repositoryRoot().appendingPathComponent("appcast.xml")) + + XCTAssertTrue(appcast.contains("sparkle-sign-warning")) + XCTAssertTrue(appcast.contains("sparkle-signatures:")) + XCTAssertTrue(appcast.contains("beta")) + XCTAssertFalse(appcast.contains("KeyPath-1.0.0.zip"), "Do not advertise an unpublished stable release.") + } + + func testReleaseScriptsGenerateAndPublishWholeSignedFeed() throws { + let root = repositoryRoot() + let sparkleLibrary = try contents(of: root.appendingPathComponent("Scripts/lib/sparkle.sh")) + let buildAndSign = try contents(of: root.appendingPathComponent("Scripts/build-and-sign.sh")) + let releaseDoctor = try contents(of: root.appendingPathComponent("Scripts/release-doctor.sh")) + let release = try contents(of: root.appendingPathComponent("Scripts/release.sh")) + + XCTAssertTrue(sparkleLibrary.contains("KEYPATH_SPARKLE_ACCOUNT=\"${KEYPATH_SPARKLE_ACCOUNT:-keypath}\"")) + XCTAssertTrue(sparkleLibrary.contains("KEYPATH_SPARKLE_PRIVATE_KEY")) + XCTAssertTrue(sparkleLibrary.contains("Curve25519.Signing.PrivateKey")) + XCTAssertTrue(sparkleLibrary.contains("sops -d \"$secrets_file\"")) + XCTAssertTrue(sparkleLibrary.contains("--ed-key-file -")) + XCTAssertFalse(sparkleLibrary.contains("--ed-key-file \"$KEYPATH_SPARKLE_PRIVATE_KEY\"")) + XCTAssertTrue(buildAndSign.contains("keypath_run_generate_appcast")) + XCTAssertTrue(buildAndSign.contains("--channel beta")) + XCTAssertTrue(buildAndSign.contains("--phased-rollout-interval 86400")) + XCTAssertTrue(buildAndSign.contains("keypath_verify_sparkle_feed")) + XCTAssertFalse(buildAndSign.contains("appcast-entry.xml")) + let sparkleCall = try XCTUnwrap(buildAndSign.range(of: "\ncreate_sparkle_archive\n")) + let deploymentGuard = try XCTUnwrap(buildAndSign.range(of: #"if [ "${SKIP_DEPLOY:-0}" = "1" ]; then"#)) + XCTAssertLessThan( + buildAndSign.distance(from: buildAndSign.startIndex, to: sparkleCall.lowerBound), + buildAndSign.distance(from: buildAndSign.startIndex, to: deploymentGuard.lowerBound), + "Sparkle artifacts must be generated before the non-deploying build exits." + ) + XCTAssertTrue(releaseDoctor.contains("keypath_verify_sparkle_signing_identity")) + XCTAssertTrue(releaseDoctor.contains("keypath_verify_sparkle_feed")) + XCTAssertTrue(release.contains(#"SPARKLE_APPCAST="dist/sparkle/appcast.xml""#)) + XCTAssertTrue(release.contains(#"ditto "$SPARKLE_APPCAST" appcast.xml"#)) + XCTAssertTrue(release.contains("KEYPATH_RELEASE_BUILD_NUMBER")) + XCTAssertTrue(release.contains(#"Set :CFBundleVersion $RELEASE_BUILD_NUMBER"#)) + XCTAssertFalse(release.contains(#"Set :CFBundleVersion $NEW_VERSION"#)) + XCTAssertFalse(release.contains("Could not find marker in appcast.xml")) + XCTAssertFalse(releaseDoctor.contains("ALLOW_UNSIGNED_SPARKLE")) + } +} + +private func repositoryRoot(file: StaticString = #filePath) -> URL { + URL(fileURLWithPath: file.description) + .deletingLastPathComponent() // BuildScripts + .deletingLastPathComponent() // KeyPathTests + .deletingLastPathComponent() // Tests + .deletingLastPathComponent() // repo root +} + +private func contents(of url: URL) throws -> String { + try String(contentsOf: url, encoding: .utf8) +} + +private func propertyList(at url: URL) throws -> [String: Any] { + let data = try Data(contentsOf: url) + let value = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) + return try XCTUnwrap(value as? [String: Any]) +} diff --git a/Tests/KeyPathTests/Lint/DeploymentScriptContractTests.swift b/Tests/KeyPathTests/Lint/DeploymentScriptContractTests.swift index a3888d311..ab8095b81 100644 --- a/Tests/KeyPathTests/Lint/DeploymentScriptContractTests.swift +++ b/Tests/KeyPathTests/Lint/DeploymentScriptContractTests.swift @@ -92,6 +92,24 @@ final class DeploymentScriptContractTests: XCTestCase { ) } + func testBuildAndSignSupportsNonDeployingCandidateBuilds() throws { + let buildAndSign = try contents(of: repositoryRoot().appendingPathComponent("Scripts/build-and-sign.sh")) + let guardText = #"if [ "${SKIP_DEPLOY:-0}" = "1" ]; then"# + + guard let guardRange = buildAndSign.range(of: guardText), + let runtimeStopRange = buildAndSign.range(of: #"pgrep -x "KeyPath""#) + else { + return XCTFail("build-and-sign must expose SKIP_DEPLOY before touching the installed runtime") + } + + XCTAssertLessThan( + buildAndSign.distance(from: buildAndSign.startIndex, to: guardRange.lowerBound), + buildAndSign.distance(from: buildAndSign.startIndex, to: runtimeStopRange.lowerBound), + "SKIP_DEPLOY must exit before stopping or replacing the installed app." + ) + XCTAssertTrue(buildAndSign.contains("Candidate retained at: $APP_BUNDLE")) + } + func testReleaseBuildReadsVersionMetadataFromPlistPaths() throws { let root = repositoryRoot() let buildAndSign = try contents(of: root.appendingPathComponent("Scripts/build-and-sign.sh")) diff --git a/Tests/KeyPathTests/Lint/PostUpdateRepairLintTests.swift b/Tests/KeyPathTests/Lint/PostUpdateRepairLintTests.swift index be1222cfb..01fc23618 100644 --- a/Tests/KeyPathTests/Lint/PostUpdateRepairLintTests.swift +++ b/Tests/KeyPathTests/Lint/PostUpdateRepairLintTests.swift @@ -9,6 +9,36 @@ import Foundation /// `runSingleAction` automatically. Users start repair explicitly from the /// normal status/wizard/CLI surfaces. final class PostUpdateRepairLintTests: XCTestCase { + func testSparkleWaitsForRuntimeShutdownBeforeInstallingUpdate() throws { + let updateService = phase1W3RepositoryRoot() + .appendingPathComponent("Sources/KeyPathAppKit/Services/UpdateService.swift") + let contents = try String(contentsOf: updateService, encoding: .utf8) + + guard let postponementBody = extractFunctionBody( + containing: "shouldPostponeRelaunchForUpdate item", + from: contents + ), let preparationBody = extractFunctionBody( + containing: "private func prepareForUpdate(version:", + from: contents + ), let willInstallBody = extractFunctionBody( + containing: "willInstallUpdate item", + from: contents + ) else { + return + } + + XCTAssertTrue(postponementBody.contains("await prepareForUpdate(version: version)")) + XCTAssertTrue(postponementBody.contains("defer")) + XCTAssertTrue(postponementBody.contains("handler.invoke()")) + XCTAssertTrue(postponementBody.contains("return true")) + XCTAssertTrue(preparationBody.contains("runSingleAction(.terminateConflictingProcesses")) + XCTAssertFalse(preparationBody.contains("run(intent: .repair")) + XCTAssertFalse( + willInstallBody.contains("prepareForUpdate"), + "Sparkle does not await willInstallUpdate; preparation must happen in its postponement callback." + ) + } + func testPostUpdateFinalizeDoesNotRunAutomaticRepair() throws { let updateService = phase1W3RepositoryRoot() .appendingPathComponent("Sources/KeyPathAppKit/Services/UpdateService.swift") @@ -48,6 +78,41 @@ final class PostUpdateRepairLintTests: XCTestCase { } } +private func extractFunctionBody( + containing signatureFragment: String, + from contents: String, + file: StaticString = #filePath, + line: UInt = #line +) -> String? { + guard let signatureRange = contents.range(of: signatureFragment) else { + XCTFail("Could not find function containing \(signatureFragment)", file: file, line: line) + return nil + } + guard let openBrace = contents[signatureRange.lowerBound...].firstIndex(of: "{") else { + XCTFail("Could not find opening brace for \(signatureFragment)", file: file, line: line) + return nil + } + + var depth = 0 + var cursor = openBrace + while cursor < contents.endIndex { + let char = contents[cursor] + if char == "{" { + depth += 1 + } else if char == "}" { + depth -= 1 + if depth == 0 { + let bodyStart = contents.index(after: openBrace) + return String(contents[bodyStart ..< cursor]) + } + } + cursor = contents.index(after: cursor) + } + + XCTFail("Could not find closing brace for \(signatureFragment)", file: file, line: line) + return nil +} + private func extractFunctionBody( named functionName: String, from contents: String, diff --git a/Tests/KeyPathTests/Services/UpdateServiceDecisionTests.swift b/Tests/KeyPathTests/Services/UpdateServiceDecisionTests.swift index c184c40dd..578879abb 100644 --- a/Tests/KeyPathTests/Services/UpdateServiceDecisionTests.swift +++ b/Tests/KeyPathTests/Services/UpdateServiceDecisionTests.swift @@ -6,7 +6,7 @@ import KeyPathWizardCore import XCTest final class UpdateServiceDecisionTests: XCTestCase { - func testPreUpdateDecisionAllowsAutomaticRepairDuringUpdateInstallWhenHelperPresent() { + func testPreUpdateDecisionRequiresRuntimeShutdownDuringUpdateInstallWhenHelperPresent() { let context = makeContext( keyPathStatus: .granted, kanataStatus: .granted, @@ -16,7 +16,7 @@ final class UpdateServiceDecisionTests: XCTestCase { ) let decision = UpdateService.preUpdateDecision(for: context) - XCTAssertEqual(decision, .automaticRepairAllowed(reason: "reason_code=services_or_helper_present")) + XCTAssertEqual(decision, .runtimeShutdownRequired(reason: "reason_code=services_or_helper_present")) } func testPreUpdateDecisionContinuesSilentlyWhenNothingRunning() { diff --git a/Tests/KeyPathTests/Services/VHIDDeviceManagerTests.swift b/Tests/KeyPathTests/Services/VHIDDeviceManagerTests.swift index 35da3163f..f359e1d30 100644 --- a/Tests/KeyPathTests/Services/VHIDDeviceManagerTests.swift +++ b/Tests/KeyPathTests/Services/VHIDDeviceManagerTests.swift @@ -1,3 +1,4 @@ +import Foundation @testable import KeyPathAppKit @testable import KeyPathCore @testable import KeyPathInstallationWizard @@ -15,36 +16,28 @@ final class VHIDDeviceManagerTests: XCTestCase { // MARK: - Version Mismatch Tests - func testHasVersionMismatch_V5InstalledRequiresV6() { - // Simulate v5.0.0 installed when v6.2.0 is required - VHIDDeviceManager.testInstalledVersionProvider = { "5.0.0" } + func testHasVersionMismatch_V7InstalledRequiresV8() { + VHIDDeviceManager.testInstalledVersionProvider = { "7.0.0" } let mgr = VHIDDeviceManager() - XCTAssertTrue(mgr.hasVersionMismatch(), "v5 installed with v6 required should be a mismatch") + XCTAssertTrue(mgr.hasVersionMismatch(), "v7 installed with v8 required should be a mismatch") } - func testHasVersionMismatch_V6InstalledRequiresV6() { - // Simulate v6.2.0 installed when v6.2.0 is required - VHIDDeviceManager.testInstalledVersionProvider = { "6.2.0" } + func testHasVersionMismatch_V8InstalledRequiresV8() { + VHIDDeviceManager.testInstalledVersionProvider = { "8.0.0" } let mgr = VHIDDeviceManager() - XCTAssertFalse(mgr.hasVersionMismatch(), "v6 installed with v6 required should NOT be a mismatch") + XCTAssertFalse(mgr.hasVersionMismatch(), "v8 installed with v8 required should NOT be a mismatch") } - func testHasVersionMismatch_V6_1InstalledRequiresV6_2() { - VHIDDeviceManager.testInstalledVersionProvider = { "6.1.0" } + func testHasVersionMismatch_V9InstalledRequiresV8() { + VHIDDeviceManager.testInstalledVersionProvider = { "9.0.0" } let mgr = VHIDDeviceManager() - XCTAssertTrue(mgr.hasVersionMismatch(), "v6.1 installed with v6.2 required should be a mismatch") + XCTAssertTrue(mgr.hasVersionMismatch(), "a different major version should be a mismatch") } - func testHasVersionMismatch_V6_0InstalledRequiresV6_2() { - VHIDDeviceManager.testInstalledVersionProvider = { "6.0.0" } + func testHasVersionMismatch_NewerCompatibleV8Release() { + VHIDDeviceManager.testInstalledVersionProvider = { "8.1.0" } let mgr = VHIDDeviceManager() - XCTAssertTrue(mgr.hasVersionMismatch(), "v6.0 installed with v6.2 required should be a mismatch") - } - - func testHasVersionMismatch_NewerCompatibleV6Release() { - VHIDDeviceManager.testInstalledVersionProvider = { "6.14.0" } - let mgr = VHIDDeviceManager() - XCTAssertFalse(mgr.hasVersionMismatch(), "a newer v6 release should remain compatible") + XCTAssertFalse(mgr.hasVersionMismatch(), "a newer v8 release should remain compatible") } func testHasVersionMismatch_NoVersionInstalled() { @@ -55,16 +48,16 @@ final class VHIDDeviceManagerTests: XCTestCase { } func testGetVersionMismatchMessage_ReturnsMessageForMismatch() { - VHIDDeviceManager.testInstalledVersionProvider = { "5.0.0" } + VHIDDeviceManager.testInstalledVersionProvider = { "7.0.0" } let mgr = VHIDDeviceManager() let message = mgr.getVersionMismatchMessage() XCTAssertNotNil(message, "Should return message when version mismatch exists") - XCTAssertTrue(message?.contains("5.0.0") ?? false, "Message should mention installed version") - XCTAssertTrue(message?.contains("6.2.0") ?? false, "Message should mention required version") + XCTAssertTrue(message?.contains("7.0.0") ?? false, "Message should mention installed version") + XCTAssertTrue(message?.contains("8.0.0") ?? false, "Message should mention required version") } func testGetVersionMismatchMessage_ReturnsNilWhenCompatible() { - VHIDDeviceManager.testInstalledVersionProvider = { "6.2.0" } + VHIDDeviceManager.testInstalledVersionProvider = { "8.0.0" } let mgr = VHIDDeviceManager() let message = mgr.getVersionMismatchMessage() XCTAssertNil(message, "Should return nil when versions are compatible") @@ -350,4 +343,23 @@ final class VHIDDeviceManagerTests: XCTestCase { "VHIDDeviceManager should use bundled version as single source of truth" ) } + + func testBundledDriverArtifactAndHelperFallbackMatchDeclaredVersion() throws { + let version = WizardSystemPaths.bundledVHIDDriverVersion + let resources = LintScanner.path("Sources/KeyPathApp/Resources") + let expectedPackage = "Karabiner-DriverKit-VirtualHIDDevice-\(version).pkg" + let bundledPackages = try FileManager.default.contentsOfDirectory(atPath: resources.path) + .filter { $0.hasPrefix("Karabiner-DriverKit-VirtualHIDDevice-") && $0.hasSuffix(".pkg") } + + XCTAssertEqual(bundledPackages, [expectedPackage], "KeyPath must ship exactly the declared VHID package.") + + let helper = try String( + contentsOf: LintScanner.path("Sources/KeyPathHelper/HelperService.swift"), + encoding: .utf8 + ) + XCTAssertTrue( + helper.contains(#"private static let requiredVHIDVersion = "\#(version)""#), + "The helper's deprecated download fallback must stay aligned with the bundled driver." + ) + } } diff --git a/appcast.xml b/appcast.xml index c557311f4..1aa96fb68 100644 --- a/appcast.xml +++ b/appcast.xml @@ -1,39 +1,30 @@ - - + KeyPath Updates https://github.com/malpern/KeyPath KeyPath keyboard remapping for macOS - automatic updates feed en - - - - Version 1.0.0-beta3 + beta 3 1.0.0-beta3 - 15.0 + 15.0.0 Fri, 16 May 2026 07:21:00 -0700 - + :root { color-scheme: light dark; } @@ -106,6 +97,8 @@

Full release notes →

]]>
-
-
+
diff --git a/docs/architecture/permission-upgrade-trust-matrix.md b/docs/architecture/permission-upgrade-trust-matrix.md index c80314195..a71247389 100644 --- a/docs/architecture/permission-upgrade-trust-matrix.md +++ b/docs/architecture/permission-upgrade-trust-matrix.md @@ -39,14 +39,24 @@ Unknown trust state (identity metadata unreadable): ## Upgrade Behavior Pre-update: -- Run soft repair only when services/helper are currently active and need a controlled stop. -- Otherwise continue silently. +- Sparkle postpones installation at its supported pre-install callback while + KeyPath stops the app-bundled Kanata runtime through `InstallerEngine`. +- KeyPath releases Sparkle's continuation after the preparation attempt; + failure is logged and handled by normal post-relaunch validation rather than + leaving the update permanently wedged. +- If no runtime or helper is active, continue silently. Post-update: - If blocking permissions are detected (`PermissionOracle`), mark hard repair. -- If helper/components/services are unhealthy, run soft repair. +- If helper/components/services are unhealthy, surface the focused repair flow + for an explicit user action; relaunch-time detection remains read-only. - If everything is healthy, continue silently. +Sparkle is the KeyPath app's release detector and installer. A KeyPath update +replaces the signed app bundle containing KeyPath, Kanata, the host bridge, and +the simulator as one tested unit. The app does not discover or install +arbitrary upstream Kanata releases independently. + ## Reason Code Logging All escalations should log a reason code for diagnosability, e.g.: diff --git a/docs/process/release-process.md b/docs/process/release-process.md index 41ee1f993..d15fecd4b 100644 --- a/docs/process/release-process.md +++ b/docs/process/release-process.md @@ -60,6 +60,16 @@ Then it delegates to `build-and-sign.sh` with fast release-candidate defaults: It deploys to `/Applications/KeyPath.app` and runs `./Scripts/verify-installed-app.sh` after the build. +To assemble and sign a local candidate without stopping or replacing the +installed KeyPath runtime, use: + +```bash +SKIP_NOTARIZE=1 SKIP_SPARKLE=1 SKIP_SNAPSHOTS=1 SKIP_DEPLOY=1 ./Scripts/build-and-sign.sh +``` + +The candidate remains at `dist/KeyPath.app`. This is the safe build-only path +for integration work that is not yet ready to control the active keyboard. + Release-candidate and public release builds hold the shared deploy lock for the whole build/sign/notarize/deploy flow. Update old worktrees before running `quick-deploy.sh`; older scripts that do not use this lock can still overwrite @@ -145,6 +155,11 @@ Use `--dry-run` before an unfamiliar release: ./Scripts/release.sh --dry-run 1.0.0 ``` +The release script keeps `CFBundleVersion` as a monotonically increasing numeric +Sparkle build number, including for versions such as `1.0.1-beta1`. It normally +increments the current build automatically; set `KEYPATH_RELEASE_BUILD_NUMBER` +only when an explicit higher build number is required. + The public release path may: 1. bump `CFBundleVersion` and `CFBundleShortVersionString` @@ -175,6 +190,9 @@ the expensive build starts: - Developer ID signing identity - notarytool keychain profile - Sparkle `sign_update` when Sparkle artifacts are enabled +- Sparkle `generate_appcast` and `generate_keys` when Sparkle artifacts are enabled +- the dedicated `keypath` signing identity matches the app's `SUPublicEDKey` +- the committed `appcast.xml` has a valid signed-feed signature - `gh-pages` worktree state when website publishing is enabled - currently installed KeyPath/Kanata runtime state - whether Poltergeist is running @@ -212,9 +230,17 @@ If another worktree owns `master`, do the pull and deploy from that worktree. ## Sparkle Notes -Sparkle auto-update artifacts require an EdDSA signature. Public release builds -fail if `sign_update` cannot produce a signature, unless -`ALLOW_UNSIGNED_SPARKLE=1` is set for local-only testing. +Sparkle auto-update artifacts and the appcast feed require EdDSA signatures. +Public release builds use Sparkle's `generate_appcast` with the dedicated +`keypath` Keychain identity, verify the complete feed, and publish that generated +file without hand-editing it. Stable releases use a one-day phased rollout; +pre-release versions are assigned to the `beta` channel. + +For unattended signing, pass `KEYPATH_SPARKLE_PRIVATE_KEY` through the process +environment. On the release machine, the helper can also load that key from the +sops-encrypted `~/dotfiles/secrets.env`. It derives and verifies the public key, +then pipes the private key to Sparkle on stdin; the key is never placed in argv, +source files, or logs. Existing users get update notifications from `appcast.xml`. Release notes linked from the appcast should remain readable in dark mode.