Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
194 commits
Select commit Hold shift + click to select a range
b576b3a
kotlin sdk + codegen done
FromWau Feb 25, 2026
ae673ba
fix
FromWau Mar 1, 2026
8016af7
wip
FromWau Mar 2, 2026
138bb8a
no reconnect
FromWau Mar 7, 2026
01abe66
use
FromWau Mar 7, 2026
8aa99e9
wip
FromWau Mar 7, 2026
cfded98
try ipv4 before ipv6
FromWau Mar 7, 2026
9ec0b8c
gradle plugin to generate bindings
FromWau Mar 7, 2026
2283a30
fix codegen
FromWau Mar 7, 2026
b692a12
cleanup
FromWau Mar 9, 2026
c0df29b
Merge branch 'master' into feat/kotlin-sdk
FromWau Mar 9, 2026
ba16972
remove nullable col
FromWau Mar 9, 2026
7d2d158
bool col
FromWau Mar 9, 2026
acd76b3
filter / where for typed
FromWau Mar 9, 2026
91527b7
remove dead OutOfEnergy
FromWau Mar 9, 2026
cccec1d
snapshot callbacks
FromWau Mar 9, 2026
5b873bf
disconnect pass error
FromWau Mar 9, 2026
66726ea
timeout
FromWau Mar 9, 2026
9ff9229
merge connect atomics
FromWau Mar 9, 2026
b5322f7
dbconnectionview interface
FromWau Mar 9, 2026
5a735e7
no double-fire cleanup
FromWau Mar 9, 2026
b0137d8
db conn
FromWau Mar 9, 2026
bf45eab
wip
FromWau Mar 9, 2026
0dead3e
remoteable generic
FromWau Mar 9, 2026
eee091b
iter is now a sequance
FromWau Mar 9, 2026
3e7eae1
update README
FromWau Mar 9, 2026
ac321e8
Added RemotePersistentTableWithPrimaryKey<Row>
FromWau Mar 9, 2026
6cdbc9d
dbconnectionview param
FromWau Mar 9, 2026
a3f0a4c
fix variant name collision
FromWau Mar 10, 2026
9e626d0
wip
FromWau Mar 11, 2026
bac6ce4
do not provide a default httpclient in sdk
FromWau Mar 11, 2026
9c07b44
rename module lib -> spacetimedb-sdk
FromWau Mar 11, 2026
0eaa29c
fixes
FromWau Mar 11, 2026
f56f01f
fixes
FromWau Mar 12, 2026
4151ce8
fixes
FromWau Mar 12, 2026
4ed37db
reconnect tests
FromWau Mar 13, 2026
1786601
reducer timeout tests
FromWau Mar 13, 2026
228cf68
fix big bsatn writes
FromWau Mar 13, 2026
d44857e
bsatn overflow tests
FromWau Mar 13, 2026
04b1497
more testing
FromWau Mar 13, 2026
7ddc5ee
event table tests
FromWau Mar 13, 2026
8df4e31
content-based keying tests
FromWau Mar 13, 2026
6d05938
codegen now escapes kotlin keywords
FromWau Mar 13, 2026
693a827
close on identity mismatch
FromWau Mar 13, 2026
4af2b78
close sendChannel after state update to closed
FromWau Mar 13, 2026
5d59ed4
set _onEndCallback BEFORE the CAS
FromWau Mar 13, 2026
9135735
getCounter add kdoc explain magic shifts
FromWau Mar 13, 2026
e7f5f9f
UniqueIndex and BTreeIndex initialization via builder
FromWau Mar 14, 2026
3ef1249
add @InternalSpacetimeApi
FromWau Mar 14, 2026
6fb1a2c
decompress payload validation
FromWau Mar 14, 2026
1f95143
cleanup codegen
FromWau Mar 14, 2026
b7cc093
use ktfmt if installed after codegen
FromWau Mar 14, 2026
57ac1d7
update db connection state
FromWau Mar 15, 2026
ba33f4a
subscribableTables
FromWau Mar 15, 2026
c3f9eb7
stats provide sync snapshot of max and min
FromWau Mar 15, 2026
a7ffd72
fire UnsubscribeApplied after removing subscriptions map
FromWau Mar 15, 2026
c902b09
rm dead isConnected from Transport
FromWau Mar 16, 2026
28d05f9
redact also stacktrace in logs
FromWau Mar 16, 2026
f5a907b
split tests
FromWau Mar 16, 2026
cbe1432
add wip integration/smoke tests
FromWau Mar 16, 2026
da5d14f
add kotlin smoketest
FromWau Mar 17, 2026
bbad7eb
add basic-kt template
FromWau Mar 18, 2026
ef1f7ab
gradle plugin: generate on any compile
FromWau Mar 18, 2026
850c2b6
gradle plugin: on clear also rm target
FromWau Mar 18, 2026
1c07dfb
add compose-kt template
FromWau Mar 18, 2026
23a75fb
disconnect not cancelable
FromWau Mar 18, 2026
7c82998
check sendChannel response
FromWau Mar 18, 2026
5359dab
callReducer with args enforce not nullable
FromWau Mar 18, 2026
39a421f
CAS loop does not create factory anymore each retry
FromWau Mar 18, 2026
5999a8e
BTreeIndex now uses set
FromWau Mar 18, 2026
7a4ecd2
early return after cleanup -> fix memory leak
FromWau Mar 18, 2026
7285ad7
set _onEndCallback after CAS
FromWau Mar 18, 2026
d82a527
sendMessage when disconnected warns now instead of exception
FromWau Mar 18, 2026
988d545
bsatnwrite: writeArrayLen checks non-negative
FromWau Mar 18, 2026
14bf09e
sqlite.float/double now not use scientific notation
FromWau Mar 18, 2026
c2b7fd5
toEpochMicroseconds now checks wire bounds
FromWau Mar 18, 2026
54c9289
compose-kt: add expect/actual defaultHost
FromWau Mar 18, 2026
4968dc5
move injection into entry modules
FromWau Mar 18, 2026
f024bb0
kotlin template: add todo for maven publish
FromWau Mar 19, 2026
573bfa4
basic-kt: add onError callback
FromWau Mar 19, 2026
9b5b043
gradle-plugin: better errors + sensible default config
FromWau Mar 19, 2026
35fd2b5
compose-kt template: fix di
FromWau Mar 19, 2026
538e37d
compose-kt template: fix build warnings
FromWau Mar 19, 2026
bda0c72
kotlin: db builder remove not needed atomic locks
FromWau Mar 19, 2026
3ba73b5
logger: fast-path for redaction
FromWau Mar 19, 2026
28952ab
buildWslUrl throw on unexpected protocol
FromWau Mar 19, 2026
1228062
kotlin: fix tests
FromWau Mar 19, 2026
e4a5c66
kotlin test neg timestamp
FromWau Mar 19, 2026
70ab5eb
kotlin: test subscribe + unsubscribe
FromWau Mar 19, 2026
4a7aa6e
kotlin: timestamp pre epoch test
FromWau Mar 19, 2026
d52c501
kotlin: test connectionId/identity large values
FromWau Mar 19, 2026
865468f
kotlin: cleanup tests
FromWau Mar 19, 2026
1bcf61b
kotlin: actually handle the sendMessage return value
FromWau Mar 19, 2026
d2bf9cd
kotlin: integrationTest flag
FromWau Mar 19, 2026
9bf1af7
kotlin: smoketest integration tests
FromWau Mar 19, 2026
8258ac7
kotlin: compose-kt template sanitize clientId input
FromWau Mar 19, 2026
11a51f2
kotlin: cleanup tests
FromWau Mar 19, 2026
0da9de8
kotlin: bsatn bounds check
FromWau Mar 19, 2026
d3ea136
kotlin: rm remoteQuery
FromWau Mar 19, 2026
6238684
kotlin: improve gradle-plugin
FromWau Mar 19, 2026
03a30e5
kotlin: subscribeToAllTables is now generated
FromWau Mar 19, 2026
1917d08
kotlin: cleanup templates
FromWau Mar 19, 2026
4217f60
kotlin: add README.md for gradle plugin
FromWau Mar 19, 2026
629df60
kotlin: rm dead dont_import param
FromWau Mar 19, 2026
e86494c
kotlin: add more redact keys for logger
FromWau Mar 19, 2026
96b422a
kotlin: fix codegen and test import
FromWau Mar 20, 2026
1418250
kotlin: strenghen tests
FromWau Mar 20, 2026
174e7cc
kotlin: fix template
FromWau Mar 20, 2026
fe6ceff
kotlin: test templates via smoketests
FromWau Mar 20, 2026
345aa25
kotlin: test cleanup
FromWau Mar 23, 2026
764990b
kotlin: rowsize hint safty require
FromWau Mar 23, 2026
bff0133
kotlin: subscribe merge with accumulated addQuery
FromWau Mar 23, 2026
f75a199
kotlin: compose-kt template. onCleared should be non cancellable
FromWau Mar 23, 2026
2469fbc
kotlin: gradle plugin clean stale files
FromWau Mar 23, 2026
07ebea7
kotlin: gradle plugin catch public exc
FromWau Mar 23, 2026
3926821
kotlin: rowsize test
FromWau Mar 23, 2026
41ec27a
kotlin: test immediate disconnect
FromWau Mar 23, 2026
7269627
kotlin: improve sendMessage tests
FromWau Mar 23, 2026
41f743f
kotlin: oneOfQuery withTimeout
FromWau Mar 23, 2026
f9c35be
kotlin: test addQuery + subscribe merge
FromWau Mar 23, 2026
8bb31d3
Int128, Int256, UInt128, UInt256 public now with integration tests
FromWau Mar 23, 2026
eec8caa
kotlin: use thread-safe CallbackList in generated reducer callbacks
FromWau Mar 23, 2026
af04475
kotlin: require(non-negative) guard to BigInteger.toHexString()
FromWau Mar 23, 2026
6fc3c3b
kotlin: add kdoc to pulic api
FromWau Mar 23, 2026
b60330c
kotlin: eventcontext expose dbconnectionview
FromWau Mar 23, 2026
775184d
kotlin: add missing col opertion extension functions
FromWau Mar 24, 2026
70623ac
ci: formatted using cargo fmt
FromWau Mar 24, 2026
26464d4
ci: fix clippy errors
FromWau Mar 24, 2026
7540120
smoketests: add also kotlin sdk unit tests
FromWau Mar 24, 2026
2ed16de
kotlin: gradle plugin provide SpacetimeConfig
FromWau Mar 25, 2026
b6bdb04
kotlin: dbconnecion does not close provided httpclient
FromWau Mar 25, 2026
fff7fcf
template: basic-kt use generated SpacetimeConfig
FromWau Mar 25, 2026
67d9c1f
template: improve compose-kt
FromWau Mar 25, 2026
7a2acab
kotlin: fail procedire callbacks on disconnect
FromWau Mar 25, 2026
68fbb11
kotlin: fire callbacks first
FromWau Mar 25, 2026
1760bf7
kotlin: do not use env DB_NAME. should use generated const
FromWau Mar 25, 2026
5e4228c
kotlin: callback first
FromWau Mar 25, 2026
cf9e267
kotlin: remove toctou
FromWau Mar 25, 2026
8d202ce
kotlin: gradle plugin use embedded groovy json
FromWau Mar 25, 2026
2a8e105
kotlin: mark transport internal
FromWau Mar 25, 2026
7f93b55
kotlin codegen: procedure args now have encode decode
FromWau Mar 25, 2026
890636b
kotlin: update gitignore
FromWau Mar 25, 2026
65527bf
smoketest: kotlin generate bindings for integration tests
FromWau Mar 25, 2026
3449b61
kotlin: rm module_bindings
FromWau Mar 25, 2026
8377b55
kotlin: update gitignore
FromWau Mar 25, 2026
a6e00d1
kotlin: add codegen tests module in sdk for smoketests
FromWau Mar 25, 2026
f014a71
kotlin codegen: panic if we want to create a enum with too many variants
FromWau Mar 25, 2026
85309f6
kotlin: IxCol now matches Col
FromWau Mar 25, 2026
944dc0b
kotlin: add brotli compression
FromWau Mar 26, 2026
2911393
kotlin: gradle plugin fix dirs cleanup/generate
FromWau Mar 26, 2026
c3b2def
codegen: kotlin cleanup
FromWau Mar 26, 2026
71cd661
fix fmt + clippy
FromWau Mar 26, 2026
a5ca7f2
detect kotlin language
FromWau Mar 26, 2026
4c68fe3
kotlin: callReducer, callProcdure mark @Internal
FromWau Mar 26, 2026
10bf500
kotlin: subscribe and addQuery do not merge
FromWau Mar 26, 2026
f9c1b29
kotlin: add gradle lock during smoketest run
FromWau Mar 26, 2026
5717a07
kotlin: tighten accessibility
FromWau Mar 26, 2026
77ef1cb
kotlin: tighten accessibility
FromWau Mar 27, 2026
9906aca
kotlin: more codegen tests
FromWau Mar 27, 2026
370ded3
kotlin: light + compression tests
FromWau Mar 27, 2026
27bc85d
kotlin: refactor to SkdResult<Success, Error>
FromWau Mar 27, 2026
162b151
Merge branch 'clockworklabs:master' into feat/kotlin-sdk
FromWau Mar 27, 2026
c1643c2
kotlin: sync fork
FromWau Mar 27, 2026
cff861d
kotlin: codegen test for PK table
FromWau Mar 27, 2026
23e993b
docs: add kotlin docs
FromWau Mar 27, 2026
7c17553
skills: add kotlin skill
FromWau Mar 27, 2026
89f3822
README: add kotlin mention
FromWau Mar 27, 2026
7247efd
Merge branch 'clockworklabs:master' into feat/kotlin-sdk
FromWau Mar 27, 2026
5da77f7
kotlin: isolate integration config
FromWau Mar 29, 2026
6850957
kotlin: provide self roled BigInteger
FromWau Mar 29, 2026
5dbfd40
kotlin: add keynote2 client bench
FromWau Mar 29, 2026
bdaf375
kotlin: update docs
FromWau Mar 29, 2026
5620651
kotlin: comment + imports cleanup
FromWau Mar 30, 2026
77647d6
kotlin: fix integration-test cargo toml
FromWau Mar 30, 2026
a08c423
kotlin: cleanup
FromWau Mar 30, 2026
d5197cd
kotlin: test use back tick names
FromWau Mar 30, 2026
47cf4da
kotlin: cleanup
FromWau Mar 30, 2026
d6baa7b
kotlin: fix sdk + plugin structure
FromWau Mar 31, 2026
c8b20b8
kotlin: cleanup
FromWau Mar 31, 2026
1575318
kotlin: cleanup
FromWau Mar 31, 2026
5b9c6e4
kotlin: cleanup
FromWau Mar 31, 2026
7ade9e9
cli: template gen add binary file support
FromWau Mar 31, 2026
71592c4
kotlin: cleanup
FromWau Mar 31, 2026
5a64909
kotlin: update doc
FromWau Mar 31, 2026
ca61ef8
kotlin: clenup test
FromWau Mar 31, 2026
8584345
kotlin: plugin gen spactime path from json
FromWau Mar 31, 2026
c368624
kotlin: fix test dep
FromWau Mar 31, 2026
3670832
kotlin: update plugin readme
FromWau Mar 31, 2026
676f095
kotlin: rename plugin -> `spacetimedb-gradle-plugin`
FromWau Mar 31, 2026
60da01f
kotlin: cleanup
FromWau Mar 31, 2026
f6aa00a
kotlin: add missing graddle-wrapper.jar for compose-kt template
FromWau Mar 31, 2026
27d739f
cli: cargo fmt
FromWau Mar 31, 2026
affd3ae
Merge branch 'master' into feat/kotlin-sdk
FromWau Mar 31, 2026
83779c7
cli: fix clippy warn
FromWau Mar 31, 2026
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ Connect from any of these platforms:
| **TypeScript** (React, Next.js, Vue, Svelte, Angular, Node.js, Bun, Deno) | [Get started](https://spacetimedb.com/docs/quickstarts/react) |
| **Rust** | [Get started](https://spacetimedb.com/docs/quickstarts/rust) |
| **C#** (standalone and Unity) | [Get started](https://spacetimedb.com/docs/quickstarts/c-sharp) |
| **Kotlin** (Android, JVM, iOS/Native) | [Get started](https://spacetimedb.com/docs/quickstarts/kotlin) |
| **C++** (Unreal Engine) | [Get started](https://spacetimedb.com/docs/quickstarts/c-plus-plus) |

## Running with Docker
Expand Down
60 changes: 53 additions & 7 deletions crates/cli/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,44 @@ fn generate_template_files() {
.push_str("pub fn get_template_files() -> HashMap<&'static str, HashMap<&'static str, &'static str>> {\n");
generated_code.push_str(" let mut templates = HashMap::new();\n\n");

let mut binary_code = String::new();
binary_code.push_str("#[allow(unused_mut)]\n");
binary_code.push_str(
"pub fn get_template_binary_files() -> HashMap<&'static str, HashMap<&'static str, &'static [u8]>> {\n",
);
binary_code.push_str(" let mut templates = HashMap::new();\n\n");

for template in &discovered_templates {
if let Some(ref server_source) = template.server_source {
let server_path = PathBuf::from(server_source);
generate_template_entry(&mut generated_code, &server_path, server_source, &manifest_dir);
generate_template_entry(
&mut generated_code,
&mut binary_code,
&server_path,
server_source,
&manifest_dir,
);
}

if let Some(ref client_source) = template.client_source {
let client_path = PathBuf::from(client_source);
generate_template_entry(&mut generated_code, &client_path, client_source, &manifest_dir);
generate_template_entry(
&mut generated_code,
&mut binary_code,
&client_path,
client_source,
&manifest_dir,
);
}
}

generated_code.push_str(" templates\n");
generated_code.push_str("}\n\n");

binary_code.push_str(" templates\n");
binary_code.push_str("}\n\n");
generated_code.push_str(&binary_code);

let repo_root = get_repo_root();
let workspace_cargo = repo_root.join("Cargo.toml");
println!("cargo:rerun-if-changed={}", workspace_cargo.display());
Expand Down Expand Up @@ -297,7 +320,17 @@ where
serializer.serialize_str(value.as_deref().unwrap_or(""))
}

fn generate_template_entry(code: &mut String, template_path: &Path, source: &str, manifest_dir: &Path) {
fn is_binary_file(path: &str) -> bool {
path.ends_with(".jar")
}

fn generate_template_entry(
code: &mut String,
binary_code: &mut String,
template_path: &Path,
source: &str,
manifest_dir: &Path,
) {
let (git_files, resolved_base) = get_git_tracked_files(template_path, manifest_dir);

if git_files.is_empty() {
Expand Down Expand Up @@ -334,6 +367,9 @@ fn generate_template_entry(code: &mut String, template_path: &Path, source: &str
code.push_str(" {\n");
code.push_str(" let mut files = HashMap::new();\n");

binary_code.push_str(" {\n");
binary_code.push_str(" let mut files = HashMap::new();\n");

for file_path in git_files {
// Example file_path: modules/chat-console-rs/src/lib.rs (relative to repo root)
// Example resolved_base: modules/chat-console-rs
Expand Down Expand Up @@ -386,15 +422,25 @@ fn generate_template_entry(code: &mut String, template_path: &Path, source: &str
// Example include_path (inside crate): "templates/basic-rs/server/src/lib.rs"
// Example include_path (outside crate): ".templates/parent_parent_modules_chat-console-rs/src/lib.rs"
// Example relative_str: "src/lib.rs"
code.push_str(&format!(
" files.insert(\"{}\", include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/{}\")));\n",
relative_str, include_path
));
if is_binary_file(&relative_str) {
binary_code.push_str(&format!(
" files.insert(\"{}\", include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/{}\")).as_slice());\n",
relative_str, include_path
));
} else {
code.push_str(&format!(
" files.insert(\"{}\", include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/{}\")));\n",
relative_str, include_path
));
}
}
}

code.push_str(&format!(" templates.insert(\"{}\", files);\n", source));
code.push_str(" }\n\n");

binary_code.push_str(&format!(" templates.insert(\"{}\", files);\n", source));
binary_code.push_str(" }\n\n");
}

/// Get a list of files tracked by git from a given directory
Expand Down
23 changes: 20 additions & 3 deletions crates/cli/src/subcommands/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use clap::Arg;
use clap::ArgAction::{Set, SetTrue};
use fs_err as fs;
use spacetimedb_codegen::{
generate, private_table_names, CodegenOptions, CodegenVisibility, Csharp, Lang, OutputFile, Rust, TypeScript,
UnrealCpp, AUTO_GENERATED_PREFIX,
generate, private_table_names, CodegenOptions, CodegenVisibility, Csharp, Kotlin, Lang, OutputFile, Rust,
TypeScript, UnrealCpp, AUTO_GENERATED_PREFIX,
};
use spacetimedb_lib::de::serde::DeserializeWrapper;
use spacetimedb_lib::{sats, RawModuleDef};
Expand All @@ -20,6 +20,7 @@ use crate::spacetime_config::{
find_and_load_with_env, CommandConfig, CommandSchema, CommandSchemaBuilder, Key, LoadedConfig, SpacetimeConfig,
};
use crate::tasks::csharp::dotnet_format;
use crate::tasks::kotlin::ktfmt;
use crate::tasks::rust::rustfmt;
use crate::util::{resolve_sibling_binary, y_or_n};
use crate::Config;
Expand Down Expand Up @@ -396,6 +397,9 @@ fn detect_default_language(client_project_dir: &Path) -> anyhow::Result<Language
if client_project_dir.join("Cargo.toml").exists() {
return Ok(Language::Rust);
}
if client_project_dir.join("build.gradle.kts").exists() || client_project_dir.join("build.gradle").exists() {
return Ok(Language::Kotlin);
}
if let Ok(entries) = fs::read_dir(client_project_dir)
&& entries
.flatten()
Expand All @@ -415,6 +419,7 @@ fn language_cli_name(lang: Language) -> &'static str {
match lang {
Language::Rust => "rust",
Language::Csharp => "csharp",
Language::Kotlin => "kotlin",
Language::TypeScript => "typescript",
Language::UnrealCpp => "unrealcpp",
}
Expand All @@ -424,6 +429,7 @@ pub fn default_out_dir_for_language(lang: Language) -> Option<PathBuf> {
match lang {
Language::Rust | Language::TypeScript => Some(PathBuf::from("src/module_bindings")),
Language::Csharp => Some(PathBuf::from("module_bindings")),
Language::Kotlin => Some(PathBuf::from("module_bindings")),
Language::UnrealCpp => None,
}
}
Expand Down Expand Up @@ -516,6 +522,7 @@ pub async fn run_prepared_generate_configs(
};
&csharp_lang as &dyn Lang
}
Language::Kotlin => &Kotlin,
Language::UnrealCpp => {
unreal_cpp_lang = UnrealCpp {
module_name: run.module_name.as_ref().unwrap(),
Expand Down Expand Up @@ -684,6 +691,7 @@ pub async fn exec_from_entries(
#[serde(rename_all = "lowercase")]
pub enum Language {
Csharp,
Kotlin,
TypeScript,
Rust,
#[serde(alias = "uecpp", alias = "ue5cpp", alias = "unreal")]
Expand All @@ -692,11 +700,18 @@ pub enum Language {

impl clap::ValueEnum for Language {
fn value_variants<'a>() -> &'a [Self] {
&[Self::Csharp, Self::TypeScript, Self::Rust, Self::UnrealCpp]
&[
Self::Csharp,
Self::Kotlin,
Self::TypeScript,
Self::Rust,
Self::UnrealCpp,
]
}
fn to_possible_value(&self) -> Option<PossibleValue> {
Some(match self {
Self::Csharp => clap::builder::PossibleValue::new("csharp").aliases(["c#", "cs"]),
Self::Kotlin => clap::builder::PossibleValue::new("kotlin").aliases(["kt", "KT"]),
Self::TypeScript => clap::builder::PossibleValue::new("typescript").aliases(["ts", "TS"]),
Self::Rust => clap::builder::PossibleValue::new("rust").aliases(["rs", "RS"]),
Self::UnrealCpp => PossibleValue::new("unrealcpp").aliases(["uecpp", "ue5cpp", "unreal"]),
Expand All @@ -710,6 +725,7 @@ impl Language {
match self {
Language::Rust => "Rust",
Language::Csharp => "C#",
Language::Kotlin => "Kotlin",
Language::TypeScript => "TypeScript",
Language::UnrealCpp => "Unreal C++",
}
Expand All @@ -719,6 +735,7 @@ impl Language {
match self {
Language::Rust => rustfmt(generated_files)?,
Language::Csharp => dotnet_format(project_dir, generated_files)?,
Language::Kotlin => ktfmt(generated_files)?,
Language::TypeScript => {
// TODO: implement formatting.
}
Expand Down
60 changes: 57 additions & 3 deletions crates/cli/src/subcommands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ pub enum ClientLanguage {
Rust,
Csharp,
TypeScript,
Kotlin,
}

impl ClientLanguage {
Expand All @@ -94,6 +95,7 @@ impl ClientLanguage {
ClientLanguage::Rust => "rust",
ClientLanguage::Csharp => "csharp",
ClientLanguage::TypeScript => "typescript",
ClientLanguage::Kotlin => "kotlin",
}
}

Expand All @@ -102,6 +104,7 @@ impl ClientLanguage {
"rust" => Ok(Some(ClientLanguage::Rust)),
"csharp" | "c#" => Ok(Some(ClientLanguage::Csharp)),
"typescript" => Ok(Some(ClientLanguage::TypeScript)),
"kotlin" | "kt" => Ok(Some(ClientLanguage::Kotlin)),
_ => Err(anyhow!("Unknown client language: {}", s)),
}
}
Expand Down Expand Up @@ -1119,6 +1122,32 @@ pub fn update_csproj_client_to_nuget(dir: &Path) -> anyhow::Result<()> {
Ok(())
}

/// Sets up a Kotlin client project: updates the project name and makes gradlew executable.
fn setup_kotlin_client(dir: &Path, project_name: &str) -> anyhow::Result<()> {
let settings_path = dir.join("settings.gradle.kts");
if settings_path.exists() {
let original = fs::read_to_string(&settings_path)?;
let re = regex::Regex::new(r#"rootProject\.name\s*=\s*"[^"]*""#).unwrap();
let updated = re
.replace(&original, &format!("rootProject.name = \"{}\"", project_name))
.to_string();
if updated != original {
fs::write(&settings_path, updated)?;
}
}

#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let gradlew = dir.join("gradlew");
if gradlew.exists() {
fs::set_permissions(&gradlew, fs::Permissions::from_mode(0o755))?;
}
}

Ok(())
}

// Helpers

fn write_if_changed(path: PathBuf, original: String, root: Element) -> anyhow::Result<()> {
Expand Down Expand Up @@ -1324,6 +1353,7 @@ fn init_builtin(config: &TemplateConfig, project_path: &Path, is_server_only: bo
.ok_or_else(|| anyhow::anyhow!("Template definition missing"))?;

let template_files = embedded::get_template_files();
let template_binary_files = embedded::get_template_binary_files();

if !is_server_only {
println!(
Expand All @@ -1332,7 +1362,7 @@ fn init_builtin(config: &TemplateConfig, project_path: &Path, is_server_only: bo
);
let client_source = &template_def.client_source;
if let Some(files) = template_files.get(client_source.as_str()) {
copy_embedded_files(files, project_path)?;
copy_embedded_files(files, template_binary_files.get(client_source.as_str()), project_path)?;
} else {
anyhow::bail!("Client template not found: {}", client_source);
}
Expand All @@ -1353,6 +1383,9 @@ fn init_builtin(config: &TemplateConfig, project_path: &Path, is_server_only: bo
Some(ClientLanguage::Csharp) => {
update_csproj_client_to_nuget(project_path)?;
}
Some(ClientLanguage::Kotlin) => {
setup_kotlin_client(project_path, &config.project_name)?;
}
None => {}
}
}
Expand All @@ -1364,7 +1397,7 @@ fn init_builtin(config: &TemplateConfig, project_path: &Path, is_server_only: bo
let server_dir = project_path.join("spacetimedb");
let server_source = &template_def.server_source;
if let Some(files) = template_files.get(server_source.as_str()) {
copy_embedded_files(files, &server_dir)?;
copy_embedded_files(files, template_binary_files.get(server_source.as_str()), &server_dir)?;
} else {
anyhow::bail!("Server template not found: {}", server_source);
}
Expand All @@ -1389,7 +1422,11 @@ fn init_builtin(config: &TemplateConfig, project_path: &Path, is_server_only: bo
Ok(())
}

fn copy_embedded_files(files: &HashMap<&str, &str>, target_dir: &Path) -> anyhow::Result<()> {
fn copy_embedded_files(
files: &HashMap<&str, &str>,
binary_files: Option<&HashMap<&str, &[u8]>>,
target_dir: &Path,
) -> anyhow::Result<()> {
for (file_path, content) in files {
// Skip .template.json files - they're only for template metadata
if file_path.ends_with(".template.json") {
Expand All @@ -1402,6 +1439,15 @@ fn copy_embedded_files(files: &HashMap<&str, &str>, target_dir: &Path) -> anyhow
}
fs::write(&full_path, content)?;
}
if let Some(binaries) = binary_files {
for (file_path, content) in binaries {
let full_path = target_dir.join(file_path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&full_path, content)?;
}
}
Ok(())
}

Expand Down Expand Up @@ -1512,6 +1558,14 @@ fn print_next_steps(config: &TemplateConfig, _project_path: &Path) -> anyhow::Re
);
println!(" spacetime generate --lang csharp --out-dir module_bindings --module-path spacetimedb");
}
(TemplateType::Builtin, Some(ServerLanguage::Rust), Some(ClientLanguage::Kotlin)) => {
println!(
" spacetime publish --module-path spacetimedb {}{}",
if config.use_local { "--server local " } else { "" },
config.project_name
);
println!(" ./gradlew run");
}
(TemplateType::Empty, _, Some(ClientLanguage::TypeScript)) => {
println!(" npm install");
if config.server_lang.is_some() {
Expand Down
31 changes: 31 additions & 0 deletions crates/cli/src/tasks/kotlin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::ffi::OsString;
use std::path::PathBuf;

use anyhow::Context;
use itertools::Itertools;

fn has_ktfmt() -> bool {
duct::cmd!("ktfmt", "--version")
.stdout_null()
.stderr_null()
.run()
.is_ok()
}

pub(crate) fn ktfmt(files: impl IntoIterator<Item = PathBuf>) -> anyhow::Result<()> {
if !has_ktfmt() {
eprintln!("ktfmt not found — skipping Kotlin formatting.");
eprintln!("Install ktfmt from https://github.com/facebook/ktfmt to auto-format generated code.");
return Ok(());
}
duct::cmd(
"ktfmt",
itertools::chain(
["--kotlinlang-style"].into_iter().map_into::<OsString>(),
files.into_iter().map_into(),
),
)
.run()
.context("ktfmt failed")?;
Ok(())
}
1 change: 1 addition & 0 deletions crates/cli/src/tasks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ pub fn build(
pub mod cpp;
pub mod csharp;
pub mod javascript;
pub mod kotlin;
pub mod rust;
Loading