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
7 changes: 5 additions & 2 deletions .github/scripts/smoke-source-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,11 @@ PLTUSERHOME="$user_home" racket -e '
"generated-clients.kotlin.path")
'

# The published archive must still generate every typed client, including the
# Kotlin client that Android applications consume from the shared tree.
# The published archive must still generate every typed client. The Kotlin
# client exists only for projects with a declared companion API (issue #311 —
# a desktop-only scaffold carries no Kotlin file nothing consumes), so declare
# the starter's `greet` RPC before generating and assert the file appears.
racket -e '(require racket/file) (define p "rivet.rktd") (define c (call-with-input-file p read)) (call-with-output-file p #:exists (quote truncate/replace) (lambda (out) (write (hash-set c (quote device-rpcs) (list (quote greet))) out)))'
PLTUSERHOME="$user_home" \
raco rivet generate
test -s .rivet/generated/kotlin/dev/rivet/generated/GeneratedBackend.kt
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ jobs:
raco rivet new KtSmoke
cd KtSmoke
cp "$GITHUB_WORKSPACE/rivet/tests/fixtures/schema-matrix-backend.rkt" app/backend.rkt
# The Kotlin client exists only for projects with a declared
# companion API (issue #311); this smoke is the Android consumer.
racket -e '(require racket/file) (define p "rivet.rktd") (define c (call-with-input-file p read)) (call-with-output-file p #:exists (quote truncate/replace) (lambda (out) (write (hash-set c (quote device-rpcs) (list (quote greet) (quote increment))) out)))'
raco rivet schema --output rivet-schema.json
raco rivet schema check rivet-schema.json --json
raco rivet generate
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ The Racket backend declares the API shared by all three native hosts:
(serve-fds in-fd out-fd))
```

`raco rivet build` reads the RPC, Event, State, Record, and Enum schema and generates typed native clients. Swift gets methods such as `increment(value:)`, `get_counter()`, and `set_counter(_:)`; C++ gets their native equivalents; Kotlin gets suspend methods such as `increment(value)` in `.rivet/generated/kotlin/dev/rivet/generated/GeneratedBackend.kt`.
`raco rivet build` reads the RPC, Event, State, Record, and Enum schema and generates typed native clients. Swift gets methods such as `increment(value:)`, `get_counter()`, and `set_counter(_:)`; C++ gets their native equivalents; Kotlin gets the same surface in `.rivet/generated/kotlin/dev/rivet/generated/GeneratedBackend.kt` for projects that declare companion `device-rpcs` (a desktop-only project has no Kotlin consumer, so the file is not part of its fresh output set).

### How it compares

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Rivet 刻意不做 WebView 框架,也不做统一跨平台控件层。Windows
(serve-fds in-fd out-fd))
```

`raco rivet build` 会读取 RPC、Event、State、Record 与 Enum schema,并生成类型化原生客户端。Swift 侧得到 `increment(value:)`、`get_counter()`、`set_counter(_:)` 等方法;C++ 侧得到对应的原生接口;Kotlin 侧在 `.rivet/generated/kotlin/dev/rivet/generated/GeneratedBackend.kt` 得到 `increment(value)` 等 suspend 方法。
`raco rivet build` 会读取 RPC、Event、State、Record 与 Enum schema,并生成类型化原生客户端。Swift 侧得到 `increment(value:)`、`get_counter()`、`set_counter(_:)` 等方法;C++ 侧得到对应的原生接口;Kotlin 侧在 `.rivet/generated/kotlin/dev/rivet/generated/GeneratedBackend.kt` 得到同样的接口——仅限声明了 companion `device-rpcs` 的项目(纯桌面项目没有 Kotlin 消费方,该文件不在其新鲜输出集内)。

### 横向对比

Expand Down
17 changes: 17 additions & 0 deletions examples/taskboard/linux/GeneratedBackend.hpp

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions examples/taskboard/windows/GeneratedBackend.hpp

Large diffs are not rendered by default.

29 changes: 20 additions & 9 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!
client-outputs
check-generated-clients
schema-snapshot
write-schema-snapshot!
Expand Down Expand Up @@ -52,8 +53,25 @@
(define codable-types (device-codable-named-types device-rpcs))
(void schema-types)
(validate-device-rpcs! device-rpcs)
;; 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. Only projects declaring a
;; companion-device API get the Kotlin client: a desktop-only product
;; has no consumer for it, and a file nobody consumes must not be a
;; required part of the fresh tree the release gate diffs against —
;; `.rivet/` is gitignored in the standard layout, so the
;; always-emitted Kotlin client failed every desktop-only product's
;; freshness check (issue #311).
(define pairs-with-kotlin
(if (null? device-rpcs)
'()
(list (cons (project-path project ".rivet" "generated" "kotlin" "dev" "rivet"
"generated" "GeneratedBackend.kt")
(generate-kotlin rpcs events states records enums device-rpcs
module-name entry-name display-name version build
identifier release-channel)))))
(define pairs
(list
(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
Expand All @@ -63,14 +81,7 @@
(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 device-rpcs
module-name entry-name display-name version build
identifier release-channel))))
pairs-with-kotlin))
(define linux-host (project-path project "linux"))
(if (directory-exists? linux-host)
(cons (cons (build-path linux-host "GeneratedBackend.hpp")
Expand Down
11 changes: 9 additions & 2 deletions rivet-cli/main.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,15 @@
(printf " ~a\n" path))))]
[(list "generate")
(define generate-clients! (command "codegen" 'generate-clients!))
(generate-clients! (current-project!))
(say "generated Swift, C++, and Kotlin clients")]
(define client-outputs (command "codegen" 'client-outputs))
(define project (current-project!))
(define outputs (car (client-outputs project)))
(generate-clients! project)
(define kotlin? (for/or ([pair (in-list outputs)])
(regexp-match? #rx"[.]kt$" (path->string (car pair)))))
(say (if kotlin?
"generated Swift, C++, and Kotlin clients"
"generated Swift and C++ clients (no companion API: no Kotlin client)"))]
[(list "generate" "--check")
;; CI freshness gate: regenerate in memory and compare with the tree,
;; so hand-edited generated files or stale version stamps cannot merge.
Expand Down
51 changes: 43 additions & 8 deletions rivet/tests/codegen.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,21 @@ RKT
(define stale-report (check-generated-clients project))
(check-equal? (hash-ref stale-report 'stale) (list windows-header))

;; Issue #311: the demo project declares no companion device RPCs, so
;; the Kotlin client is not part of the fresh output set at all —
;; desktop-only products keep `.rivet/` gitignored and still pass the
;; gate without committing a file nothing consumes.
(define kotlin-generated
(build-path project-root ".rivet" "generated" "kotlin" "dev" "rivet"
"generated" "GeneratedBackend.kt"))
(delete-file kotlin-generated)
(check-false
(for/or ([pair (in-list (car (client-outputs project)))])
(string=? (path->string (car pair)) (path->string kotlin-generated))))

(delete-file windows-header)
(define missing-report (check-generated-clients project))
(check-equal? (hash-ref missing-report 'missing) (list kotlin-generated))
(check-equal? (hash-ref missing-report 'stale) (list windows-header))
(check-equal? (hash-ref missing-report 'missing) (list windows-header))
(check-equal? (hash-ref missing-report 'stale) '())

(void (generate-clients! project))
(define repaired-report (check-generated-clients project))
Expand Down Expand Up @@ -213,11 +221,6 @@ RKT
(define linux-cpp
(file->string
(build-path project-root "linux" "GeneratedBackend.hpp")))
(define kotlin
(file->string
(build-path project-root
".rivet" "generated" "kotlin" "dev" "rivet" "generated"
"GeneratedBackend.kt")))

(check-true (string-contains? swift "public protocol RivetGeneratedRPCClient: Sendable"))
(check-true (string-contains? swift "extension RivetClient: RivetGeneratedRPCClient {}"))
Expand Down Expand Up @@ -272,6 +275,21 @@ RKT
(regexp-match? #rx"rivet::linux_runtime::CallResult rivet_result" linux-cpp))
(check-false (regexp-match? #rx"rivet::windows" linux-cpp))

;; Issue #311: declare a companion API so the Kotlin client joins the
;; fresh output set, then generate and assert its content. (The fresh-set
;; assertion above proves a desktop-only project omits the file.)
(define config-path (build-path project-root "rivet.rktd"))
(define base-config (call-with-input-file config-path read))
(call-with-output-file config-path
#:exists 'truncate
(lambda (out) (write (hash-set base-config 'device-rpcs '(greet)) out)))
(set! project (load-project project-root))
(generate-clients! project)
(define kotlin
(file->string
(build-path project-root
".rivet" "generated" "kotlin" "dev" "rivet" "generated"
"GeneratedBackend.kt")))
;; The typed Kotlin client targets the coroutine runtime. State accessors
;; are package-level extension functions, so the imports are load-bearing.
(check-true (regexp-match? #rx"package dev.rivet.generated" kotlin))
Expand Down Expand Up @@ -309,6 +327,13 @@ RKT
(check-true
(regexp-match? #rx"\"progress\" -> Progress\\(decode_Int64\\(value\\)\\)" kotlin))

;; Restore the desktop-only manifest: the remaining sections reuse the
;; same project binding with backends that declare no `greet`.
(call-with-output-file config-path
#:exists 'truncate
(lambda (out) (write (hash-set base-config 'device-rpcs '()) out)))
(set! project (load-project project-root))

;; A versioned snapshot is suitable for source control and CI. Additions are
;; compatible, while changing a published signature is reported and fails.
(define baseline-path (build-path project-root "schema-baseline.json"))
Expand Down Expand Up @@ -458,6 +483,13 @@ RKT
(check-regexp-match #rx"func echo_role\\(role: RivetTypes.Role\\) async throws -> RivetTypes.Role" enum-swift)
(check-regexp-match #rx"enum class Role \\{ admin, member \\};" enum-cpp)
(check-regexp-match #rx"std::future<Role> echo_role\\(Role role\\)" enum-cpp)
;; The Kotlin checks need a companion API for the client to exist at all
;; (issue #311); declaring one also switches Swift into the RivetTypes
;; device surface, so this flip happens after the Swift/C++ assertions.
(call-with-output-file config-path #:exists 'truncate
(lambda (out) (write (hash-set base-config 'device-rpcs '(echo-role)) out)))
(set! project (load-project project-root))
(generate-clients! project)
(define enum-kotlin
(file->string
(build-path project-root
Expand All @@ -467,6 +499,9 @@ RKT
(check-regexp-match #rx"admin\\(\"admin\"\\)," enum-kotlin)
(check-regexp-match #rx"suspend fun echo_role\\(role: Role\\): Role" enum-kotlin)
(check-regexp-match #rx"Role\\.fromWireName" enum-kotlin)
(call-with-output-file config-path #:exists 'truncate
(lambda (out) (write (hash-set base-config 'device-rpcs '()) out)))
(set! project (load-project project-root))

(define enum-baseline-path (build-path project-root "enum-schema.json"))
(write-schema-snapshot! project enum-baseline-path)
Expand Down
26 changes: 20 additions & 6 deletions rivet/tests/device-codegen.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,22 @@
RKT
out)))

;; Issue #311: with no declared companion-device API the project has no
;; Kotlin consumer, so the fresh output set must not require the Kotlin
;; client — desktop-only products keep `.rivet/` gitignored and must
;; still pass the release freshness gate.
(check-false
(for/or ([pair (in-list (car (client-outputs (load-project project-root))))])
(string-contains? (path->string (car pair)) "GeneratedBackend.kt")))

;; Companion APIs are default-deny and only the configured RPCs appear in
;; the generated Codable request/client/router surface.
(set-device-rpcs! '(fetch-profile refresh))
(define project (load-project project-root))
(generate-clients! project)
(check-true
(for/or ([pair (in-list (car (client-outputs project)))])
(string-contains? (path->string (car pair)) "GeneratedBackend.kt")))
(define swift
(file->string
(build-path project-root "macos-host" "Sources" "RivetHost"
Expand Down Expand Up @@ -103,12 +114,15 @@ RKT
(define exported-baseline (build-path project-root ".rivet" "device-exported.json"))
(write-schema-snapshot! project exported-baseline)
(set-device-rpcs! '())
(generate-clients! (load-project project-root))
(define no-device-kotlin
(file->string
(build-path project-root ".rivet" "generated" "kotlin" "dev" "rivet"
"generated" "GeneratedBackend.kt")))
(check-false (string-contains? no-device-kotlin "class RivetDeviceAPI"))
;; Issue #311: an empty companion API means no Kotlin consumer — the
;; fresh output set drops the Kotlin client entirely. A previously
;; generated file may linger on disk, but it is no longer part of the
;; contract the release freshness gate diffs against.
(define empty-device-project (load-project project-root))
(check-false
(for/or ([pair (in-list (car (client-outputs empty-device-project)))])
(string-contains? (path->string (car pair)) "GeneratedBackend.kt")))
(generate-clients! empty-device-project)
(define removal-report
(check-schema-compatibility! (load-project project-root) exported-baseline))
(check-false (hash-ref removal-report 'compatible))
Expand Down
Loading