cluaupp 1.5.2-cf → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/cluaupp.js +0 -36
- package/examples/game/src/Client/Controllers/DataController.client.clpp +2 -2
- package/examples/game/src/Client/Main.client.clpp +1 -1
- package/examples/game/src/Modules/Core/Guard.server.clpp +2 -2
- package/examples/game/src/Modules/Core/PlayerHandler.server.clpp +2 -2
- package/examples/game/src/Server/Boot/DataBoot.server.clpp +3 -3
- package/examples/game/src/Server/Main.server.clpp +1 -1
- package/package.json +2 -6
- package/templates/game/src/Client/Controllers/DataController.client.clpp +2 -2
- package/templates/game/src/Client/Main.client.clpp +1 -1
- package/templates/game/src/Modules/Core/Guard.server.clpp +2 -2
- package/templates/game/src/Modules/Core/PlayerHandler.server.clpp +2 -2
- package/templates/game/src/Server/Boot/DataBoot.server.clpp +3 -3
- package/templates/game/src/Server/Main.server.clpp +1 -1
- package/editors/vscode/syntaxes/clpp.tmLanguage.json +0 -90
- package/native/bin/.gitkeep +0 -1
- package/rust/cluaupp/Cargo.lock +0 -795
- package/rust/cluaupp/Cargo.toml +0 -17
- package/rust/cluaupp/src/main.rs +0 -717
- package/scripts/postinstall-native.js +0 -52
package/rust/cluaupp/src/main.rs
DELETED
|
@@ -1,717 +0,0 @@
|
|
|
1
|
-
use anyhow::{anyhow, Context, Result};
|
|
2
|
-
use clap::{Parser, Subcommand};
|
|
3
|
-
use notify::{Config as NotifyConfig, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
|
4
|
-
use regex::Regex;
|
|
5
|
-
use serde::{Deserialize, Serialize};
|
|
6
|
-
use serde_json::json;
|
|
7
|
-
use sha2::{Digest, Sha256};
|
|
8
|
-
use std::collections::HashSet;
|
|
9
|
-
use std::ffi::OsStr;
|
|
10
|
-
use std::fs;
|
|
11
|
-
use std::path::{Path, PathBuf};
|
|
12
|
-
use std::process::Stdio;
|
|
13
|
-
use std::time::Duration;
|
|
14
|
-
use tokio::io::AsyncWriteExt;
|
|
15
|
-
use tokio::process::Command;
|
|
16
|
-
use tokio::sync::Semaphore;
|
|
17
|
-
use walkdir::WalkDir;
|
|
18
|
-
|
|
19
|
-
#[derive(Parser)]
|
|
20
|
-
#[command(name = "cluaupp", about = "Cluaupp native orchestrator CLI")]
|
|
21
|
-
struct Cli {
|
|
22
|
-
#[command(subcommand)]
|
|
23
|
-
command: Commands,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
#[derive(Subcommand)]
|
|
27
|
-
enum Commands {
|
|
28
|
-
Init {
|
|
29
|
-
#[arg(default_value = ".")]
|
|
30
|
-
folder: String,
|
|
31
|
-
},
|
|
32
|
-
Build {
|
|
33
|
-
#[arg(default_value = ".")]
|
|
34
|
-
folder: String,
|
|
35
|
-
#[arg(long, default_value = "default.project.json")]
|
|
36
|
-
rojo: String,
|
|
37
|
-
#[arg(long, default_value_t = true)]
|
|
38
|
-
incremental: bool,
|
|
39
|
-
#[arg(short = 'j', long)]
|
|
40
|
-
jobs: Option<usize>,
|
|
41
|
-
},
|
|
42
|
-
Watch {
|
|
43
|
-
#[arg(default_value = ".")]
|
|
44
|
-
folder: String,
|
|
45
|
-
#[arg(long, default_value = "default.project.json")]
|
|
46
|
-
rojo: String,
|
|
47
|
-
#[arg(short = 'j', long)]
|
|
48
|
-
jobs: Option<usize>,
|
|
49
|
-
},
|
|
50
|
-
Install {
|
|
51
|
-
#[arg(default_value = ".")]
|
|
52
|
-
_folder: String,
|
|
53
|
-
},
|
|
54
|
-
UpdateTypes {
|
|
55
|
-
#[arg(default_value = ".")]
|
|
56
|
-
folder: String,
|
|
57
|
-
},
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
61
|
-
struct ProjectConfig {
|
|
62
|
-
#[serde(default = "default_root_dir", rename = "rootDir")]
|
|
63
|
-
root_dir: String,
|
|
64
|
-
#[serde(default = "default_out_dir", rename = "outDir")]
|
|
65
|
-
out_dir: String,
|
|
66
|
-
#[serde(default)]
|
|
67
|
-
strict: bool,
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
#[derive(Debug, Deserialize)]
|
|
71
|
-
struct ManifestToml {
|
|
72
|
-
compiler: Option<ManifestCompiler>,
|
|
73
|
-
dependencies: Option<HashMapToml>,
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
#[derive(Debug, Deserialize)]
|
|
77
|
-
struct ManifestCompiler {
|
|
78
|
-
#[serde(default, rename = "root_dir")]
|
|
79
|
-
root_dir: Option<String>,
|
|
80
|
-
#[serde(default, rename = "out_dir")]
|
|
81
|
-
out_dir: Option<String>,
|
|
82
|
-
#[serde(default)]
|
|
83
|
-
strict: Option<bool>,
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
type HashMapToml = std::collections::BTreeMap<String, toml::Value>;
|
|
87
|
-
|
|
88
|
-
fn default_root_dir() -> String {
|
|
89
|
-
"Src".to_string()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
fn default_out_dir() -> String {
|
|
93
|
-
"out".to_string()
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
#[derive(Debug, Deserialize)]
|
|
97
|
-
struct CompileArtifact {
|
|
98
|
-
ok: bool,
|
|
99
|
-
luau: String,
|
|
100
|
-
#[serde(default, rename = "fileName")]
|
|
101
|
-
file_name: String,
|
|
102
|
-
#[serde(default)]
|
|
103
|
-
error: String,
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
#[derive(Debug, Serialize, Deserialize)]
|
|
107
|
-
struct CacheEntry {
|
|
108
|
-
source_hash: String,
|
|
109
|
-
out_rel: String,
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
#[tokio::main]
|
|
113
|
-
async fn main() -> Result<()> {
|
|
114
|
-
let cli = Cli::parse();
|
|
115
|
-
match cli.command {
|
|
116
|
-
Commands::Init { folder } => {
|
|
117
|
-
let dest = std::env::current_dir()?.join(folder);
|
|
118
|
-
init_project(&dest)?;
|
|
119
|
-
}
|
|
120
|
-
Commands::Build {
|
|
121
|
-
folder,
|
|
122
|
-
rojo,
|
|
123
|
-
incremental,
|
|
124
|
-
jobs,
|
|
125
|
-
} => {
|
|
126
|
-
let root = std::env::current_dir()?.join(folder);
|
|
127
|
-
build_project(&root, &rojo, incremental, jobs).await?;
|
|
128
|
-
}
|
|
129
|
-
Commands::Watch { folder, rojo, jobs } => {
|
|
130
|
-
let root = std::env::current_dir()?.join(folder);
|
|
131
|
-
watch_project(root, rojo, jobs).await?;
|
|
132
|
-
}
|
|
133
|
-
Commands::Install { .. } => {
|
|
134
|
-
install_dependencies(std::env::current_dir()?.as_path())?;
|
|
135
|
-
}
|
|
136
|
-
Commands::UpdateTypes { folder } => {
|
|
137
|
-
let root = std::env::current_dir()?.join(folder);
|
|
138
|
-
let status = Command::new("node")
|
|
139
|
-
.arg("generated/cli.js")
|
|
140
|
-
.arg("api")
|
|
141
|
-
.arg("generate")
|
|
142
|
-
.current_dir(&root)
|
|
143
|
-
.status()
|
|
144
|
-
.await
|
|
145
|
-
.context("failed to run node generated/cli.js api generate")?;
|
|
146
|
-
if !status.success() {
|
|
147
|
-
return Err(anyhow!("cluaupp update-types failed"));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
Ok(())
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
fn init_project(dest: &Path) -> Result<()> {
|
|
155
|
-
let home = cluaupp_home()?;
|
|
156
|
-
let template = home.join("templates").join("game");
|
|
157
|
-
let include = home.join("include");
|
|
158
|
-
let runtime = home.join("runtime");
|
|
159
|
-
|
|
160
|
-
copy_dir_all(&template, dest)?;
|
|
161
|
-
copy_dir_all(&include, &dest.join("include"))?;
|
|
162
|
-
if runtime.exists() {
|
|
163
|
-
copy_dir_all(&runtime, &dest.join("libs"))?;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
let cfg = load_config(dest)?;
|
|
167
|
-
sync_rojo_tree(dest, &cfg, "default.project.json")?;
|
|
168
|
-
println!("Cluaupp ready in {}", dest.display());
|
|
169
|
-
Ok(())
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
fn install_dependencies(root: &Path) -> Result<()> {
|
|
173
|
-
let libs_root = dirs_home()?.join(".clpp").join("libs");
|
|
174
|
-
fs::create_dir_all(&libs_root)?;
|
|
175
|
-
let manifest = root.join("clpp.toml");
|
|
176
|
-
if !manifest.exists() {
|
|
177
|
-
println!("cluaupp install: no clpp.toml, created {}", libs_root.display());
|
|
178
|
-
return Ok(());
|
|
179
|
-
}
|
|
180
|
-
let parsed: ManifestToml = toml::from_str(&fs::read_to_string(&manifest)?)?;
|
|
181
|
-
let deps = parsed.dependencies.unwrap_or_default();
|
|
182
|
-
if deps.is_empty() {
|
|
183
|
-
println!("cluaupp install: no [dependencies] in clpp.toml");
|
|
184
|
-
return Ok(());
|
|
185
|
-
}
|
|
186
|
-
for (name, value) in deps {
|
|
187
|
-
let dep_dir = libs_root.join(&name);
|
|
188
|
-
fs::create_dir_all(&dep_dir)?;
|
|
189
|
-
let marker = dep_dir.join(".source");
|
|
190
|
-
fs::write(marker, value.to_string())?;
|
|
191
|
-
println!("cluaupp install: linked {name}");
|
|
192
|
-
}
|
|
193
|
-
Ok(())
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async fn build_project(root: &Path, rojo: &str, incremental: bool, jobs: Option<usize>) -> Result<()> {
|
|
197
|
-
let cfg = load_config(root)?;
|
|
198
|
-
let src_dir = root.join(&cfg.root_dir);
|
|
199
|
-
let out_dir = root.join(&cfg.out_dir);
|
|
200
|
-
fs::create_dir_all(&out_dir)?;
|
|
201
|
-
sync_rojo_tree(root, &cfg, rojo)?;
|
|
202
|
-
|
|
203
|
-
let files = collect_sources(&src_dir);
|
|
204
|
-
if files.is_empty() {
|
|
205
|
-
return Err(anyhow!("no .clpp/.clp/.clh files in {}", src_dir.display()));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
let clpp_bin = resolve_clpp_bin();
|
|
209
|
-
let jobs = jobs.unwrap_or_else(|| std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4));
|
|
210
|
-
let sem = std::sync::Arc::new(Semaphore::new(jobs.max(1)));
|
|
211
|
-
|
|
212
|
-
let mut tasks = Vec::new();
|
|
213
|
-
for file in files {
|
|
214
|
-
let permit = sem.clone().acquire_owned().await?;
|
|
215
|
-
let root = root.to_path_buf();
|
|
216
|
-
let src_dir = src_dir.clone();
|
|
217
|
-
let out_dir = out_dir.clone();
|
|
218
|
-
let cfg = cfg.clone();
|
|
219
|
-
let clpp_bin = clpp_bin.clone();
|
|
220
|
-
let file_path = file.clone();
|
|
221
|
-
let rojo_name = rojo.to_string();
|
|
222
|
-
tasks.push(tokio::spawn(async move {
|
|
223
|
-
let _permit = permit;
|
|
224
|
-
compile_one(&root, &src_dir, &out_dir, &cfg, &clpp_bin, &file_path, incremental, &rojo_name).await
|
|
225
|
-
}));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
for task in tasks {
|
|
229
|
-
task.await??;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
sync_rojo_tree(root, &cfg, rojo)?;
|
|
233
|
-
Ok(())
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
async fn watch_project(root: PathBuf, rojo: String, jobs: Option<usize>) -> Result<()> {
|
|
237
|
-
let cfg = load_config(&root)?;
|
|
238
|
-
let src_dir = root.join(&cfg.root_dir);
|
|
239
|
-
if !src_dir.exists() {
|
|
240
|
-
fs::create_dir_all(&src_dir)?;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
build_project(&root, &rojo, true, jobs).await?;
|
|
244
|
-
println!("cluaupp watch {}", src_dir.display());
|
|
245
|
-
|
|
246
|
-
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Event>();
|
|
247
|
-
let mut watcher = RecommendedWatcher::new(
|
|
248
|
-
move |res| {
|
|
249
|
-
if let Ok(event) = res {
|
|
250
|
-
let _ = tx.send(event);
|
|
251
|
-
}
|
|
252
|
-
},
|
|
253
|
-
NotifyConfig::default(),
|
|
254
|
-
)?;
|
|
255
|
-
watcher.watch(&src_dir, RecursiveMode::Recursive)?;
|
|
256
|
-
|
|
257
|
-
while let Some(first) = rx.recv().await {
|
|
258
|
-
if !is_relevant_event(&first.kind) {
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
let debounce = Duration::from_millis(350);
|
|
262
|
-
loop {
|
|
263
|
-
match tokio::time::timeout(debounce, rx.recv()).await {
|
|
264
|
-
Ok(Some(next)) if is_relevant_event(&next.kind) => continue,
|
|
265
|
-
_ => break,
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if let Err(err) = build_project(&root, &rojo, true, jobs).await {
|
|
269
|
-
eprintln!("{err}");
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
Ok(())
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
fn is_relevant_event(kind: &EventKind) -> bool {
|
|
276
|
-
matches!(
|
|
277
|
-
kind,
|
|
278
|
-
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) | EventKind::Any
|
|
279
|
-
)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async fn compile_one(
|
|
283
|
-
root: &Path,
|
|
284
|
-
src_dir: &Path,
|
|
285
|
-
out_dir: &Path,
|
|
286
|
-
cfg: &ProjectConfig,
|
|
287
|
-
clpp_bin: &str,
|
|
288
|
-
file_path: &Path,
|
|
289
|
-
incremental: bool,
|
|
290
|
-
_rojo: &str,
|
|
291
|
-
) -> Result<()> {
|
|
292
|
-
let rel = normalize(pathdiff(file_path, src_dir)?);
|
|
293
|
-
let source = fs::read_to_string(file_path)?;
|
|
294
|
-
let source_hash = fingerprint_source_with_deps(&source, file_path, src_dir, cfg);
|
|
295
|
-
let out_rel = to_luau_path(&rel);
|
|
296
|
-
let out_file = out_dir.join(&out_rel);
|
|
297
|
-
let cache_file = cache_file_for(root, &rel);
|
|
298
|
-
|
|
299
|
-
if incremental
|
|
300
|
-
&& cache_file.exists()
|
|
301
|
-
&& out_file.exists()
|
|
302
|
-
&& is_cache_hit(&cache_file, &source_hash, &out_rel)?
|
|
303
|
-
{
|
|
304
|
-
return Ok(());
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
let request = json!({
|
|
308
|
-
"source": source,
|
|
309
|
-
"fileName": normalize(file_path.to_path_buf()),
|
|
310
|
-
"strict": cfg.strict,
|
|
311
|
-
"optimize": true
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
let mut cmd = Command::new(clpp_bin);
|
|
315
|
-
cmd.arg("api")
|
|
316
|
-
.arg("compile")
|
|
317
|
-
.stdin(Stdio::piped())
|
|
318
|
-
.stdout(Stdio::piped())
|
|
319
|
-
.stderr(Stdio::piped())
|
|
320
|
-
.current_dir(root);
|
|
321
|
-
let mut child = cmd.spawn().context("failed to spawn clpp api compile")?;
|
|
322
|
-
if let Some(stdin) = child.stdin.as_mut() {
|
|
323
|
-
stdin.write_all(request.to_string().as_bytes()).await?;
|
|
324
|
-
}
|
|
325
|
-
let output = child.wait_with_output().await?;
|
|
326
|
-
if !output.status.success() {
|
|
327
|
-
return Err(anyhow!(
|
|
328
|
-
"{}",
|
|
329
|
-
String::from_utf8_lossy(&output.stderr).trim().to_string()
|
|
330
|
-
));
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
334
|
-
let artifact: CompileArtifact = serde_json::from_str(stdout.trim())
|
|
335
|
-
.with_context(|| format!("invalid clpp artifact for {rel}"))?;
|
|
336
|
-
if !artifact.ok {
|
|
337
|
-
let message = if !artifact.error.is_empty() {
|
|
338
|
-
artifact.error
|
|
339
|
-
} else {
|
|
340
|
-
format!("clpp compile failed for {}", artifact.file_name)
|
|
341
|
-
};
|
|
342
|
-
return Err(anyhow!(message));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
let mut luau = artifact.luau;
|
|
346
|
-
luau = rewrite_clpp_libs(luau);
|
|
347
|
-
luau = rewrite_game_rooted_requires(luau);
|
|
348
|
-
luau = ensure_game_services(luau);
|
|
349
|
-
if cfg.strict && !luau.starts_with("--!strict") {
|
|
350
|
-
luau = format!("--!strict\n{luau}");
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if let Some(parent) = out_file.parent() {
|
|
354
|
-
fs::create_dir_all(parent)?;
|
|
355
|
-
}
|
|
356
|
-
fs::write(&out_file, luau)?;
|
|
357
|
-
write_cache(&cache_file, &CacheEntry { source_hash, out_rel })?;
|
|
358
|
-
println!("cluaupp: {} -> {}", rel, normalize(pathdiff(&out_file, root)?));
|
|
359
|
-
Ok(())
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
fn rewrite_clpp_libs(input: String) -> String {
|
|
363
|
-
let re = Regex::new(r#"require\(ClppLibs\.([A-Za-z_][A-Za-z0-9_]*)\)"#).expect("regex");
|
|
364
|
-
re.replace_all(&input, r#"require(ReplicatedStorage.CluauppLibs.$1)"#)
|
|
365
|
-
.to_string()
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
fn rewrite_game_rooted_requires(input: String) -> String {
|
|
369
|
-
let mut next = input;
|
|
370
|
-
let services = [
|
|
371
|
-
"ReplicatedStorage",
|
|
372
|
-
"ReplicatedFirst",
|
|
373
|
-
"ServerScriptService",
|
|
374
|
-
"ServerStorage",
|
|
375
|
-
"StarterPlayer",
|
|
376
|
-
"StarterGui",
|
|
377
|
-
"Workspace",
|
|
378
|
-
];
|
|
379
|
-
for svc in services {
|
|
380
|
-
let re = Regex::new(&format!(r"script(?:\.Parent)+\.{}\.", svc)).expect("regex");
|
|
381
|
-
next = re.replace_all(&next, format!("{svc}.")).to_string();
|
|
382
|
-
}
|
|
383
|
-
for folder in ["Include", "Modules", "Declarations"] {
|
|
384
|
-
let re = Regex::new(&format!(r"script(?:\.Parent)+\.{}\.", folder)).expect("regex");
|
|
385
|
-
next = re
|
|
386
|
-
.replace_all(&next, format!("ReplicatedStorage.{folder}."))
|
|
387
|
-
.to_string();
|
|
388
|
-
}
|
|
389
|
-
next
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
fn ensure_game_services(input: String) -> String {
|
|
393
|
-
let mut needed = Vec::new();
|
|
394
|
-
for svc in [
|
|
395
|
-
"ReplicatedStorage",
|
|
396
|
-
"ReplicatedFirst",
|
|
397
|
-
"ServerScriptService",
|
|
398
|
-
"ServerStorage",
|
|
399
|
-
"StarterPlayer",
|
|
400
|
-
] {
|
|
401
|
-
let need = Regex::new(&format!(r"require\({}\.", svc))
|
|
402
|
-
.expect("regex")
|
|
403
|
-
.is_match(&input);
|
|
404
|
-
let has = Regex::new(&format!(r#"GetService\(\s*"{}"\s*\)"#, svc))
|
|
405
|
-
.expect("regex")
|
|
406
|
-
.is_match(&input);
|
|
407
|
-
if need && !has {
|
|
408
|
-
needed.push(svc);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
if needed.is_empty() {
|
|
412
|
-
return input;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
let block = needed
|
|
416
|
-
.iter()
|
|
417
|
-
.map(|svc| format!(r#"const {} = game:GetService("{}")"#, svc, svc))
|
|
418
|
-
.collect::<Vec<_>>()
|
|
419
|
-
.join("\n");
|
|
420
|
-
|
|
421
|
-
let mut lines = input.lines().collect::<Vec<_>>();
|
|
422
|
-
let mut insert_at = 0usize;
|
|
423
|
-
while insert_at < lines.len() && lines[insert_at].starts_with("--") {
|
|
424
|
-
insert_at += 1;
|
|
425
|
-
}
|
|
426
|
-
lines.insert(insert_at, "");
|
|
427
|
-
lines.insert(insert_at + 1, &block);
|
|
428
|
-
lines.join("\n")
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
fn is_cache_hit(cache_file: &Path, source_hash: &str, out_rel: &str) -> Result<bool> {
|
|
432
|
-
let text = fs::read_to_string(cache_file)?;
|
|
433
|
-
let cached: CacheEntry = serde_json::from_str(&text)?;
|
|
434
|
-
Ok(cached.source_hash == source_hash && cached.out_rel == out_rel)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
fn write_cache(path: &Path, entry: &CacheEntry) -> Result<()> {
|
|
438
|
-
if let Some(parent) = path.parent() {
|
|
439
|
-
fs::create_dir_all(parent)?;
|
|
440
|
-
}
|
|
441
|
-
fs::write(path, serde_json::to_vec(entry)?)?;
|
|
442
|
-
Ok(())
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
fn cache_file_for(root: &Path, rel: &str) -> PathBuf {
|
|
446
|
-
let key = hex_sha(rel.as_bytes());
|
|
447
|
-
root.join(".cluaupp")
|
|
448
|
-
.join("compile-cache")
|
|
449
|
-
.join(format!("{key}.json"))
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
fn fingerprint_source_with_deps(source: &str, file_path: &Path, src_dir: &Path, cfg: &ProjectConfig) -> String {
|
|
453
|
-
let mut hasher = Sha256::new();
|
|
454
|
-
hasher.update(cfg.strict.to_string().as_bytes());
|
|
455
|
-
hasher.update(source.as_bytes());
|
|
456
|
-
for dep in resolve_language_deps(source, file_path, src_dir) {
|
|
457
|
-
hasher.update(dep.to_string_lossy().as_bytes());
|
|
458
|
-
if let Ok(content) = fs::read(&dep) {
|
|
459
|
-
hasher.update(content);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
hex_bytes(&hasher.finalize())
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
fn resolve_language_deps(source: &str, file_path: &Path, src_dir: &Path) -> Vec<PathBuf> {
|
|
466
|
-
let mut deps = Vec::new();
|
|
467
|
-
let mut seen = HashSet::new();
|
|
468
|
-
let link_re = Regex::new(r#"(?m)^\s*link\s+["']([^"']+)["'](?:\s+as\s+[A-Za-z_][A-Za-z0-9_]*)?\s*;?\s*$"#)
|
|
469
|
-
.expect("regex");
|
|
470
|
-
|
|
471
|
-
for cap in link_re.captures_iter(source) {
|
|
472
|
-
if let Some(path_match) = cap.get(1) {
|
|
473
|
-
if let Some(dep) = resolve_dep(path_match.as_str(), file_path, src_dir) {
|
|
474
|
-
let key = normalize(dep.clone());
|
|
475
|
-
if seen.insert(key) {
|
|
476
|
-
deps.push(dep);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
deps
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
fn resolve_dep(raw: &str, from_file: &Path, src_dir: &Path) -> Option<PathBuf> {
|
|
485
|
-
let from_dir = from_file.parent()?;
|
|
486
|
-
let candidates = [
|
|
487
|
-
from_dir.join(raw),
|
|
488
|
-
src_dir.join(raw),
|
|
489
|
-
from_dir.join(format!("{raw}.clh")),
|
|
490
|
-
src_dir.join(format!("{raw}.clh")),
|
|
491
|
-
];
|
|
492
|
-
candidates.into_iter().find(|p| p.is_file())
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
fn collect_sources(root: &Path) -> Vec<PathBuf> {
|
|
496
|
-
WalkDir::new(root)
|
|
497
|
-
.into_iter()
|
|
498
|
-
.filter_map(|e| e.ok())
|
|
499
|
-
.filter(|e| e.file_type().is_file())
|
|
500
|
-
.map(|e| e.into_path())
|
|
501
|
-
.filter(|p| is_source_file(p.as_path()))
|
|
502
|
-
.collect()
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
fn is_source_file(path: &Path) -> bool {
|
|
506
|
-
matches!(
|
|
507
|
-
path.extension().and_then(OsStr::to_str).map(|v| v.to_ascii_lowercase()),
|
|
508
|
-
Some(ext) if ext == "clpp" || ext == "clp" || ext == "clh"
|
|
509
|
-
)
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
fn to_luau_path(rel: &str) -> String {
|
|
513
|
-
Regex::new(r"\.(clpp|clp|clh)$")
|
|
514
|
-
.expect("regex")
|
|
515
|
-
.replace(rel, ".luau")
|
|
516
|
-
.to_string()
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
fn load_config(root: &Path) -> Result<ProjectConfig> {
|
|
520
|
-
let manifest = root.join("clpp.toml");
|
|
521
|
-
if manifest.exists() {
|
|
522
|
-
let parsed: ManifestToml = toml::from_str(&fs::read_to_string(&manifest)?)?;
|
|
523
|
-
if let Some(compiler) = parsed.compiler {
|
|
524
|
-
return Ok(ProjectConfig {
|
|
525
|
-
root_dir: compiler.root_dir.unwrap_or_else(default_root_dir),
|
|
526
|
-
out_dir: compiler.out_dir.unwrap_or_else(default_out_dir),
|
|
527
|
-
strict: compiler.strict.unwrap_or(false),
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
let cfg1 = root.join("cluaupp.config.json");
|
|
532
|
-
let cfg2 = root.join("cluau.config.json");
|
|
533
|
-
if cfg1.exists() {
|
|
534
|
-
let parsed: ProjectConfig = serde_json::from_str(&fs::read_to_string(&cfg1)?)?;
|
|
535
|
-
return Ok(parsed);
|
|
536
|
-
}
|
|
537
|
-
if cfg2.exists() {
|
|
538
|
-
let parsed: ProjectConfig = serde_json::from_str(&fs::read_to_string(&cfg2)?)?;
|
|
539
|
-
return Ok(parsed);
|
|
540
|
-
}
|
|
541
|
-
Ok(ProjectConfig {
|
|
542
|
-
root_dir: default_root_dir(),
|
|
543
|
-
out_dir: default_out_dir(),
|
|
544
|
-
strict: false,
|
|
545
|
-
})
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
fn sync_rojo_tree(root: &Path, cfg: &ProjectConfig, rojo_file: &str) -> Result<()> {
|
|
549
|
-
let tree = build_rojo_tree(root, &cfg.root_dir, &cfg.out_dir)?;
|
|
550
|
-
let doc = json!({
|
|
551
|
-
"name": "cluaupp-game",
|
|
552
|
-
"tree": tree
|
|
553
|
-
});
|
|
554
|
-
fs::write(root.join(rojo_file), format!("{}\n", serde_json::to_string_pretty(&doc)?))?;
|
|
555
|
-
Ok(())
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
fn build_rojo_tree(root: &Path, root_dir: &str, out_dir: &str) -> Result<serde_json::Value> {
|
|
559
|
-
let out_root = root.join(out_dir);
|
|
560
|
-
let src_root = root.join(root_dir);
|
|
561
|
-
let source_root = if out_root.exists() { out_root } else { src_root };
|
|
562
|
-
let rel_root = normalize(pathdiff(&source_root, root)?);
|
|
563
|
-
|
|
564
|
-
let mut tree = json!({
|
|
565
|
-
"$className": "DataModel",
|
|
566
|
-
"ServerScriptService": { "$className": "ServerScriptService", "$path": format!("{}/Server", rel_root) },
|
|
567
|
-
"ReplicatedStorage": {
|
|
568
|
-
"$className": "ReplicatedStorage",
|
|
569
|
-
"CluauppLibs": { "$path": "libs" },
|
|
570
|
-
"Declarations": { "$path": format!("{}/Declarations", rel_root) },
|
|
571
|
-
"Include": { "$path": format!("{}/Include", rel_root) },
|
|
572
|
-
},
|
|
573
|
-
"StarterPlayer": {
|
|
574
|
-
"$className": "StarterPlayer",
|
|
575
|
-
"StarterPlayerScripts": {
|
|
576
|
-
"$className": "StarterPlayerScripts",
|
|
577
|
-
"$path": format!("{}/Client", rel_root)
|
|
578
|
-
}
|
|
579
|
-
},
|
|
580
|
-
"HttpService": {
|
|
581
|
-
"$className": "HttpService",
|
|
582
|
-
"$properties": { "HttpEnabled": true }
|
|
583
|
-
}
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
tree["ServerScriptService"]["Parallel"] = json!({ "$path": format!("{}/Parallel", rel_root) });
|
|
587
|
-
tree["ServerScriptService"]["Modules"] = json!({});
|
|
588
|
-
tree["StarterPlayer"]["StarterPlayerScripts"]["Modules"] = json!({});
|
|
589
|
-
tree["ReplicatedStorage"]["Modules"] = json!({});
|
|
590
|
-
|
|
591
|
-
let modules_root = source_root.join("Modules");
|
|
592
|
-
if modules_root.exists() {
|
|
593
|
-
for entry in WalkDir::new(&modules_root).into_iter().filter_map(|e| e.ok()) {
|
|
594
|
-
if !entry.file_type().is_file() {
|
|
595
|
-
continue;
|
|
596
|
-
}
|
|
597
|
-
let file = entry.path();
|
|
598
|
-
let ext = file.extension().and_then(OsStr::to_str).unwrap_or_default().to_ascii_lowercase();
|
|
599
|
-
if !["luau", "lua", "clpp", "clp", "clh"].contains(&ext.as_str()) {
|
|
600
|
-
continue;
|
|
601
|
-
}
|
|
602
|
-
let module_rel = normalize(pathdiff(file, &modules_root)?);
|
|
603
|
-
let stem = module_stem(&module_rel);
|
|
604
|
-
if stem.is_empty() {
|
|
605
|
-
continue;
|
|
606
|
-
}
|
|
607
|
-
let out_rel = normalize(pathdiff(file, root)?);
|
|
608
|
-
let is_server = Regex::new(r"\.server\.(luau|lua|clpp|clp|clh)$")
|
|
609
|
-
.expect("regex")
|
|
610
|
-
.is_match(&module_rel);
|
|
611
|
-
let is_client = Regex::new(r"\.client\.(luau|lua|clpp|clp|clh)$")
|
|
612
|
-
.expect("regex")
|
|
613
|
-
.is_match(&module_rel);
|
|
614
|
-
let target = if is_server {
|
|
615
|
-
&mut tree["ServerScriptService"]["Modules"]
|
|
616
|
-
} else if is_client {
|
|
617
|
-
&mut tree["StarterPlayer"]["StarterPlayerScripts"]["Modules"]
|
|
618
|
-
} else {
|
|
619
|
-
&mut tree["ReplicatedStorage"]["Modules"]
|
|
620
|
-
};
|
|
621
|
-
set_nested_path(target, &stem, &out_rel);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
Ok(tree)
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
fn module_stem(rel: &str) -> String {
|
|
628
|
-
let re = Regex::new(r"(\.server|\.client)?\.(luau|lua|clpp|clp|clh)$").expect("regex");
|
|
629
|
-
re.replace(rel, "").to_string()
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
fn set_nested_path(node: &mut serde_json::Value, stem: &str, fs_path: &str) {
|
|
633
|
-
let parts: Vec<&str> = stem.split('/').filter(|p| !p.is_empty()).collect();
|
|
634
|
-
if parts.is_empty() {
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
let mut cur = node;
|
|
638
|
-
for part in &parts[..parts.len() - 1] {
|
|
639
|
-
if cur.get(*part).is_none() {
|
|
640
|
-
cur[*part] = json!({});
|
|
641
|
-
}
|
|
642
|
-
cur = &mut cur[*part];
|
|
643
|
-
}
|
|
644
|
-
cur[parts[parts.len() - 1]] = json!({ "$path": fs_path });
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
|
|
648
|
-
if !src.exists() {
|
|
649
|
-
return Ok(());
|
|
650
|
-
}
|
|
651
|
-
for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) {
|
|
652
|
-
let rel = entry.path().strip_prefix(src).unwrap_or(entry.path());
|
|
653
|
-
let target = dst.join(rel);
|
|
654
|
-
if entry.file_type().is_dir() {
|
|
655
|
-
fs::create_dir_all(&target)?;
|
|
656
|
-
} else if entry.file_type().is_file() {
|
|
657
|
-
if let Some(parent) = target.parent() {
|
|
658
|
-
fs::create_dir_all(parent)?;
|
|
659
|
-
}
|
|
660
|
-
fs::copy(entry.path(), &target)?;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
Ok(())
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
fn resolve_clpp_bin() -> String {
|
|
667
|
-
std::env::var("CLPP")
|
|
668
|
-
.or_else(|_| std::env::var("CLPP_PATH"))
|
|
669
|
-
.unwrap_or_else(|_| "clpp".to_string())
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
fn cluaupp_home() -> Result<PathBuf> {
|
|
673
|
-
if let Ok(home) = std::env::var("CLUAUPP_HOME") {
|
|
674
|
-
return Ok(PathBuf::from(home));
|
|
675
|
-
}
|
|
676
|
-
let exe = std::env::current_exe()?;
|
|
677
|
-
let base = exe
|
|
678
|
-
.parent()
|
|
679
|
-
.and_then(|p| p.parent())
|
|
680
|
-
.map(|p| p.to_path_buf())
|
|
681
|
-
.ok_or_else(|| anyhow!("failed to resolve CLUAUPP_HOME"))?;
|
|
682
|
-
Ok(base)
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
fn dirs_home() -> Result<PathBuf> {
|
|
686
|
-
if let Ok(home) = std::env::var("HOME") {
|
|
687
|
-
return Ok(PathBuf::from(home));
|
|
688
|
-
}
|
|
689
|
-
if let Ok(home) = std::env::var("USERPROFILE") {
|
|
690
|
-
return Ok(PathBuf::from(home));
|
|
691
|
-
}
|
|
692
|
-
Err(anyhow!("failed to resolve home directory"))
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
fn pathdiff(path: &Path, base: &Path) -> Result<PathBuf> {
|
|
696
|
-
path.strip_prefix(base)
|
|
697
|
-
.map(|p| p.to_path_buf())
|
|
698
|
-
.map_err(|_| anyhow!("failed relative path"))
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
fn normalize(path: PathBuf) -> String {
|
|
702
|
-
path.to_string_lossy().replace('\\', "/")
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
fn hex_sha(bytes: &[u8]) -> String {
|
|
706
|
-
let mut hasher = Sha256::new();
|
|
707
|
-
hasher.update(bytes);
|
|
708
|
-
hex_bytes(&hasher.finalize())
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
fn hex_bytes(bytes: &[u8]) -> String {
|
|
712
|
-
let mut out = String::with_capacity(bytes.len() * 2);
|
|
713
|
-
for b in bytes {
|
|
714
|
-
out.push_str(&format!("{:02x}", b));
|
|
715
|
-
}
|
|
716
|
-
out
|
|
717
|
-
}
|