Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ checksum and there is no `raco pkg install rivet@<version>` selector. The
standard hold-back form is the tag-pinned Git source shown above. Racket treats
the fragment as the Git revision while retaining normal dependency resolution.

Never leave the fragment empty. `...rivet.git#` with a bare `#` does not fail
the install: Racket resolves it to the default branch, so CI compiles against
moving `main` while the workflow believed it was pinned — an undefined
workflow variable has shipped releases built this way. Always pin a tag or a
commit SHA after `#`, and run `raco rivet doctor --release` before publishing;
the release gate fails on empty refs, on tag refs that do not match the
installed rivet release, on stale generated files, and on host source that
calls the update API with keywords the installed rivet does not accept.

Rivet itself additionally launches the generated GTK4 package inside a
headless Weston compositor with `GDK_BACKEND=wayland`. This is a native
Wayland gate: `DISPLAY` is unset, so the application cannot silently pass by
Expand Down
7 changes: 7 additions & 0 deletions docs/ci.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ checksum,也不存在 `raco pkg install rivet@<version>` 这种历史版本选
标准的版本保留方式就是上面带标签的 Git 来源;Racket 会把片段解释为 Git
revision,同时照常解析依赖。

绝不要让片段留空。`...rivet.git#` 这种裸 `#` 不会让安装失败:Racket 会把它
解析到默认分支,CI 于是对着移动的 `main` 编译,而工作流却以为自己在用固定
版本——曾经有过未定义的工作流变量产生空片段、并以此发版的事故。`#` 之后
永远固定到标签或 commit SHA,并在发布前运行 `raco rivet doctor --release`;
发布门禁会对空片段、与已安装 rivet 版本不符的标签片段、过期的生成文件、
以及用已安装 rivet 不接受的关键字调用更新 API 的宿主源码报错。

对安全要求高的产品,应把 action 标签换成该标签对应的完整 commit SHA,并在
包 URL 片段中使用同一个 commit。该 action 会下载官方 minimal Racket CS
源码归档、校验 SHA-256、按
Expand Down
29 changes: 29 additions & 0 deletions docs/release-and-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,35 @@ The lower-level `make-install-plan`, `execute-install-plan!`, and `recover-insta

Build metadata does not affect version precedence.

## Update API keyword contract

The hand-written Racket side of the updater is a small, stable surface. These
are the complete keyword contracts; a keyword not listed here is rejected at
runtime, which is exactly how shipped releases have failed on user machines.
`raco rivet doctor --release` scans host source against the installed rivet's
own contracts, so drift fails in CI instead:

- `fetch-update-manifest` accepts `#:key-id` `#:maximum-bytes` `#:connect-timeout-seconds` `#:idle-timeout-seconds`.
- `select-update` accepts `#:install-kind`.
- `download-update` accepts `#:on-progress` `#:progress-every` `#:stall-timeout-seconds` `#:total-timeout-seconds`.
- `fetch-sibling-asset` accepts `#:sha256` `#:maximum-bytes` `#:on-progress` `#:connect-timeout-seconds` `#:idle-timeout-seconds`.

Adding a keyword to one of these functions is a compatible addition for the
contract scanner; removing or renaming one is breaking and needs a release
note.

## Release gate

Run `raco rivet doctor --release` in release CI before building installers.
One command and one exit code cover: generated client files match a fresh
regeneration (`raco rivet generate --check` runs that single check
standalone), the committed schema baseline accepts the current backend,
hand-written host sources call the update API only with keywords the
installed rivet accepts, and every `rivet.git#...` pin in the repository is
non-empty and consistent with the installed rivet release. A failing gate
means the release would otherwise ship a stale version stamp, a schema
break, or an updater call that fails only on user machines.

## Host-side updates on macOS

The engine API above covers applications whose update flow runs in Racket. Applications whose macOS host owns the whole flow natively should not hand-roll it: the Swift package exposes `RivetUpdate` (`platform/macos`), a shared update client for installed `.app` bundles. It performs the authenticated check against the same signed wrapper the engine verifies (`RivetUpdateService.check`), a bounded delegate-driven artifact download with progress and cancellation (`downloadAndInstall`), SHA-256 and size verification against the signed manifest, `ditto` unpacking, a bundle-version confirmation, and an atomic swap-with-rollback that relaunches the application.
Expand Down
107 changes: 70 additions & 37 deletions rivet-cli/codegen.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"codegen/type-graph.rkt")

(provide generate-clients!
check-generated-clients
schema-snapshot
write-schema-snapshot!
check-schema-compatibility!)
Expand All @@ -20,7 +21,12 @@
(call-with-output-file path #:exists 'truncate/replace
(lambda (out) (display content out))))

(define (generate-clients! project)
;; Computes the full set of generated client files as (path . content) pairs
;; without touching the disk, plus the schema summary generate-clients!
;; returns. generate-clients! writes these pairs; check-generated-clients
;; diffs them against the tree, so the freshness gate and the writer can
;; never disagree about what a fresh tree looks like.
(define (client-outputs project)
(define backend (project-path project (project-ref project 'backend)))
(define-values (rpcs events states records enums)
(load-schema backend #:id (project-identifier project)))
Expand All @@ -37,41 +43,68 @@
(when (and (null? rpcs) (null? events) (null? states) (null? records) (null? enums))
(error 'generate-clients! "the backend declares no RPCs, Events, shared states, Records, or Enums"))
(validate-native-identifiers! rpcs events states records enums device-rpcs)
(parameterize ([current-records records]
[current-enums enums])
;; Force dependency ordering first so recursive record graphs fail before
;; companion-channel Codable analysis attempts to walk them.
(define schema-types (all-types rpcs events states records enums))
(define codable-types (device-codable-named-types device-rpcs))
(void schema-types)
(validate-device-rpcs! device-rpcs)
(parameterize ([current-swift-codable-types codable-types])
(write-generated!
(project-path project "macos-host" "Sources" "RivetHost" "GeneratedBackend.swift")
(generate-swift rpcs events states records enums device-rpcs
module-name entry-name display-name version build
identifier release-channel)))
(write-generated!
(project-path project "windows" "GeneratedBackend.hpp")
(generate-cpp rpcs events states records enums
module-name entry-name display-name version build
identifier release-channel "rivet::windows"))
(define linux-host (project-path project "linux"))
(when (directory-exists? linux-host)
(write-generated!
(build-path linux-host "GeneratedBackend.hpp")
(generate-cpp rpcs events states records enums
module-name entry-name display-name version build
identifier release-channel "rivet::linux_runtime")))
;; Android consumes the typed client from the shared generated tree until
;; generated Compose projects exist; the package layout keeps the file
;; drop-in for Gradle source sets.
(write-generated!
(project-path project ".rivet" "generated" "kotlin" "dev" "rivet"
"generated" "GeneratedBackend.kt")
(generate-kotlin rpcs events states records enums
module-name entry-name display-name version build
identifier release-channel)))
(define outputs
(parameterize ([current-records records]
[current-enums enums])
;; Force dependency ordering first so recursive record graphs fail before
;; companion-channel Codable analysis attempts to walk them.
(define schema-types (all-types rpcs events states records enums))
(define codable-types (device-codable-named-types device-rpcs))
(void schema-types)
(validate-device-rpcs! device-rpcs)
(define pairs
(list
(cons (project-path project "macos-host" "Sources" "RivetHost" "GeneratedBackend.swift")
(parameterize ([current-swift-codable-types codable-types])
(generate-swift rpcs events states records enums device-rpcs
module-name entry-name display-name version build
identifier release-channel)))
(cons (project-path project "windows" "GeneratedBackend.hpp")
(generate-cpp rpcs events states records enums
module-name entry-name display-name version build
identifier release-channel "rivet::windows"))
;; Android consumes the typed client from the shared generated tree
;; until generated Compose projects exist; the package layout keeps
;; the file drop-in for Gradle source sets.
(cons (project-path project ".rivet" "generated" "kotlin" "dev" "rivet"
"generated" "GeneratedBackend.kt")
(generate-kotlin rpcs events states records enums
module-name entry-name display-name version build
identifier release-channel))))
(define linux-host (project-path project "linux"))
(if (directory-exists? linux-host)
(cons (cons (build-path linux-host "GeneratedBackend.hpp")
(generate-cpp rpcs events states records enums
module-name entry-name display-name version build
identifier release-channel "rivet::linux_runtime"))
pairs)
pairs)))
(cons outputs (list rpcs states events records enums)))

(define (generate-clients! project)
(define plan (client-outputs project))
(for ([pair (in-list (car plan))])
(write-generated! (car pair) (cdr pair)))
;; Preserve the historical first two result positions for callers that
;; inspect codegen output programmatically; Events, Records, and Enums follow.
(list rpcs states events records enums))
(cdr plan))

;; The release freshness gate (issue #245): regenerate in memory and compare
;; with the tree. Because the regenerated content always carries the current
;; rivet.rktd version/build stamp, a hand-edited version literal or a
;; forgotten regeneration after a version bump shows up as a stale file.
(define (check-generated-clients project)
(define outputs (car (client-outputs project)))
(define fresh '())
(define stale '())
(define missing '())
(for ([pair (in-list outputs)])
(define path (car pair))
(cond
[(not (file-exists? path)) (set! missing (cons path missing))]
[(not (string=? (file->string path) (cdr pair)))
(set! stale (cons path stale))]
[else (set! fresh (cons path fresh))]))
(hash 'fresh (reverse fresh)
'stale (reverse stale)
'missing (reverse missing)))
Loading
Loading