use crate::compiler::write_if_changed;
use anyhow::Result;
use std::fs;
use std::path::{Path, PathBuf};
use super::dependency_management::{
dep_spec_to_cargo_line, detect_external_crate_deps, find_windjammer_runtime_path,
path_to_toml_string, propagate_source_cargo_deps, walk_rs_files,
};
use super::feature_management::{wasm_output_needs_runtime, WEB_SYS_CARGO_FEATURES};
pub(crate) fn find_wj_config(source_dir: &Path) -> crate::config::WjConfig {
let mut dir = source_dir;
loop {
let candidate = dir.join("wj.toml");
if candidate.exists() {
if let Ok(cfg) = crate::config::WjConfig::load_from_file(&candidate) {
return cfg;
}
}
match dir.parent() {
Some(parent) if parent != dir => dir = parent,
_ => break,
}
}
crate::config::WjConfig::default()
}
pub(crate) fn write_cargo_toml(
output_dir: &Path,
source_dir: &Path,
lib_or_bin_section: &str,
) -> Result<()> {
let wj_config = find_wj_config(source_dir);
let runtime_path = find_windjammer_runtime_path();
let runtime_path_str = path_to_toml_string(&runtime_path);
let mut deps = vec![
format!("windjammer-runtime = {{ path = \"{}\" }}", runtime_path_str),
"smallvec = \"1.13\"".to_string(),
"serde = { version = \"1.0\", features = [\"derive\"] }".to_string(),
];
let external_deps = detect_external_crate_deps(output_dir, source_dir);
deps.extend(external_deps);
let propagated = propagate_source_cargo_deps(source_dir, &deps);
deps.extend(propagated);
let existing_dep_names: std::collections::HashSet<String> = deps
.iter()
.filter_map(|d| d.split('=').next().map(|n| n.trim().to_string()))
.collect();
for (name, spec) in &wj_config.dependencies {
if !existing_dep_names.contains(name) {
deps.push(dep_spec_to_cargo_line(name, spec));
}
}
let project_name = infer_project_name(source_dir);
let inferred_snake = project_name.replace('-', "_");
let config_name = wj_config
.project
.as_ref()
.map(|p| &p.name)
.filter(|n| !n.is_empty())
.or_else(|| {
let n = &wj_config.package.name;
if n.is_empty() {
None
} else {
Some(n)
}
});
let package_name = if let Some(name) = config_name {
name.replace('-', "_")
} else {
resolve_package_name_with_existing_cargo(output_dir, &inferred_snake)
};
let package_name_underscore = package_name.replace('-', "_");
deps.retain(|dep| {
let dep_name = dep.split('=').next().unwrap_or("").trim();
let dep_name_underscore = dep_name.replace('-', "_");
dep_name_underscore != package_name_underscore
});
let deps_section = format!("[dependencies]\n{}\n\n", deps.join("\n"));
let dev_deps_section = if wj_config.dev_dependencies.is_empty() {
String::new()
} else {
let lines: Vec<String> = wj_config
.dev_dependencies
.iter()
.map(|(name, spec)| dep_spec_to_cargo_line(name, spec))
.collect();
format!("[dev-dependencies]\n{}\n\n", lines.join("\n"))
};
let cargo_toml = format!(
r#"# Auto-generated by Windjammer compiler - do not edit manually
[package]
name = "{}"
version = "0.1.0"
edition = "2021"
# Prevent this from being treated as part of parent workspace
[workspace]
{}{}{}[profile.release]
opt-level = 3
"#,
package_name, deps_section, dev_deps_section, lib_or_bin_section
);
let cargo_toml_path = output_dir.join("Cargo.toml");
write_if_changed(&cargo_toml_path, &cargo_toml)?;
Ok(())
}
pub(crate) fn resolve_wasm_lib_path(output_dir: &Path) -> Result<String> {
if output_dir.join("lib.rs").exists() {
return Ok("lib.rs".to_string());
}
if output_dir.join("mod.rs").exists() {
return Ok("mod.rs".to_string());
}
let mut top_level: Vec<PathBuf> = Vec::new();
if let Ok(entries) = fs::read_dir(output_dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.extension().and_then(|s| s.to_str()) == Some("rs") {
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
if name != "main.rs" {
top_level.push(p);
}
}
}
}
top_level.sort();
if let Some(p) = top_level.first() {
return Ok(p
.file_name()
.expect("wasm lib path")
.to_string_lossy()
.into_owned());
}
let all = walk_rs_files(output_dir)?;
if all.len() == 1 {
let rel = all[0].strip_prefix(output_dir)?;
return Ok(rel.to_string_lossy().replace('\\', "/"));
}
for p in &all {
if p.file_name().and_then(|s| s.to_str()) == Some("lib.rs") {
let rel = p.strip_prefix(output_dir)?;
return Ok(rel.to_string_lossy().replace('\\', "/"));
}
}
if all.is_empty() {
anyhow::bail!("No Rust sources found under output dir for WASM Cargo.toml");
}
anyhow::bail!(
"Cannot pick a single WASM library entry point among {} Rust files",
all.len()
)
}
pub fn generate_wasm_cargo_toml(output_dir: &Path, source_dir: &Path) -> Result<()> {
let lib_rel = resolve_wasm_lib_path(output_dir)?;
let needs_runtime = wasm_output_needs_runtime(output_dir)?;
let runtime_line = if needs_runtime {
let runtime_path = find_windjammer_runtime_path();
let runtime_str = path_to_toml_string(&runtime_path);
format!(
"windjammer-runtime = {{ path = \"{}\", features = [\"wasm\"] }}\n",
runtime_str
)
} else {
String::new()
};
let extra_deps = detect_external_crate_deps(output_dir, source_dir);
let smallvec_line = "smallvec = \"1.13\"\n";
let extra_section = if extra_deps.is_empty() {
String::new()
} else {
format!("{}\n", extra_deps.join("\n"))
};
let project_snake = infer_project_name(source_dir).replace('-', "_");
let package_name =
resolve_package_name_with_existing_cargo(output_dir, &format!("{project_snake}_wasm"));
let cargo_toml = format!(
r#"# Auto-generated by Windjammer compiler - do not edit manually
[package]
name = "{pkg}"
version = "0.1.0"
edition = "2021"
# Prevent this from being treated as part of parent workspace
[workspace]
[lib]
crate-type = ["cdylib"]
path = "{lib_rel}"
[dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
serde-wasm-bindgen = "0.6"
web-sys = {{ version = "0.3", features = [
{web_sys_features}
] }}
js-sys = "0.3"
serde = {{ version = "1.0", features = ["derive"] }}
serde_json = "1.0"
console_error_panic_hook = "0.1"
{smallvec}{runtime}{extra}
[profile.release]
opt-level = "z"
lto = true
"#,
pkg = package_name,
lib_rel = lib_rel,
web_sys_features = WEB_SYS_CARGO_FEATURES,
smallvec = smallvec_line,
runtime = runtime_line,
extra = extra_section,
);
write_if_changed(&output_dir.join("Cargo.toml"), &cargo_toml)?;
Ok(())
}
pub(crate) fn read_package_name_from_package_section(content: &str) -> Option<String> {
let mut in_package = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed == "[package]" {
in_package = true;
continue;
}
if trimmed.starts_with('[') && trimmed != "[package]" {
in_package = false;
}
if in_package && trimmed.starts_with("name") {
let rest = trimmed.strip_prefix("name")?.trim_start();
let rest = rest.strip_prefix('=')?.trim();
let value = rest.strip_prefix('"').and_then(|s| s.strip_suffix('"'))?;
return Some(value.to_string());
}
}
None
}
pub(crate) fn resolve_package_name_with_existing_cargo(
output_dir: &Path,
inferred_snake: &str,
) -> String {
let existing_cargo = output_dir.join("Cargo.toml");
if !existing_cargo.exists() {
return inferred_snake.to_string();
}
let Ok(content) = fs::read_to_string(&existing_cargo) else {
return inferred_snake.to_string();
};
match read_package_name_from_package_section(&content) {
Some(name) if name != "windjammer" => name.replace('-', "_"),
_ => inferred_snake.to_string(),
}
}
pub fn infer_project_name_from(source_dir: &Path) -> String {
infer_project_name(source_dir)
}
pub(crate) fn infer_project_name(source_dir: &Path) -> String {
let config_files = ["wj.toml", "game.toml"];
let dirs_to_check: Vec<&Path> = {
let mut v = vec![source_dir];
if let Some(parent) = source_dir.parent() {
v.push(parent);
}
v
};
for dir in &dirs_to_check {
for config_name in &config_files {
let config_path = dir.join(config_name);
if config_path.exists() {
if let Ok(content) = fs::read_to_string(&config_path) {
if let Some(name) = extract_package_name_from_toml(&content) {
return name;
}
}
}
}
}
if let Some(dir_name) = source_dir.file_name().and_then(|n| n.to_str()) {
if dir_name != "src" {
return sanitize_package_name(&dir_name.to_lowercase().replace(' ', "-"));
}
if let Some(parent) = source_dir.parent() {
if let Some(parent_name) = parent.file_name().and_then(|n| n.to_str()) {
return sanitize_package_name(&parent_name.to_lowercase().replace(' ', "-"));
}
}
}
"windjammer".to_string()
}
fn sanitize_package_name(name: &str) -> String {
let stripped: String = name
.chars()
.skip_while(|c| !c.is_alphabetic() && *c != '_')
.map(|c| {
if c.is_alphanumeric() || c == '_' || c == '-' {
c
} else {
'_'
}
})
.collect();
if stripped.is_empty() {
"windjammer".to_string()
} else {
stripped
}
}
fn extract_package_name_from_toml(content: &str) -> Option<String> {
let mut in_package = false;
let mut found_any_section = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
found_any_section = true;
in_package = trimmed == "[package]";
continue;
}
if trimmed.starts_with("name") {
if in_package || !found_any_section {
return trimmed
.split('"')
.nth(1)
.map(|s| s.to_lowercase().replace(' ', "-"));
}
}
}
None
}