use crate::GeneratorContext;
use anyhow::anyhow;
use camino::Utf8Path;
#[cfg(feature = "external-skeleton")]
use camino::Utf8PathBuf;
#[cfg(not(feature = "external-skeleton"))]
use include_dir::{Dir, include_dir};
use std::borrow::Cow;
use toml_edit::{Array, DocumentMut, value};
#[cfg(not(feature = "external-skeleton"))]
static SKELETON: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/skeleton");
#[cfg(feature = "external-skeleton")]
fn skeleton_root() -> Utf8PathBuf {
Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("skeleton")
}
#[cfg(not(feature = "external-skeleton"))]
fn skeleton_cargo_toml() -> anyhow::Result<Cow<'static, str>> {
Ok(Cow::Borrowed(
SKELETON
.get_file("Cargo.toml_")
.or_else(|| SKELETON.get_file("Cargo.toml"))
.ok_or_else(|| anyhow!("Missing Cargo.toml skeleton"))?
.contents_utf8()
.ok_or_else(|| anyhow!("Cargo.toml skeleton is not valid UTF-8"))?,
))
}
#[cfg(feature = "external-skeleton")]
fn skeleton_cargo_toml() -> anyhow::Result<Cow<'static, str>> {
let root = skeleton_root();
let primary = root.join("Cargo.toml_");
match std::fs::read_to_string(&primary) {
Ok(contents) => Ok(Cow::Owned(contents)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
std::fs::read_to_string(root.join("Cargo.toml"))
.map(Cow::Owned)
.map_err(|error| anyhow!("Failed to read Cargo.toml skeleton: {error}"))
}
Err(error) => Err(anyhow!(
"Failed to read Cargo.toml skeleton {primary}: {error}"
)),
}
}
pub fn generate_cargo_toml(context: &GeneratorContext<'_>) -> anyhow::Result<()> {
let cargo_toml = skeleton_cargo_toml()?;
let mut doc = cargo_toml
.parse::<DocumentMut>()
.map_err(|err| anyhow!("Cargo.toml skeleton is not a valid TOML: {err}"))?;
change_package_name(context, &mut doc);
if context.target.is_p3() {
set_p3_default_features(&mut doc);
}
let output_path = context.output.join("Cargo.toml");
crate::write_if_changed(output_path, doc.to_string())?;
Ok(())
}
fn change_package_name(context: &GeneratorContext, doc: &mut DocumentMut) {
let crate_name = &context.world_name;
doc["package"]["name"] = value(crate_name);
}
fn set_p3_default_features(doc: &mut DocumentMut) {
let mut default = Array::new();
default.push("p3");
default.push("normal-p3");
doc["features"]["default"] = value(default);
}
const GENERATED_FILES: &[&str] = &["src/lib.rs"];
#[cfg(not(feature = "external-skeleton"))]
pub fn copy_skeleton_lock(output: &Utf8Path, package_name: &str) -> anyhow::Result<()> {
if let Some(lock_file) = SKELETON.get_file("Cargo.lock") {
let dest = output.join("Cargo.lock");
let contents = lock_file
.contents_utf8()
.ok_or_else(|| anyhow!("Embedded skeleton Cargo.lock contains invalid UTF-8"))?;
let skeleton_package = skeleton_package_name()?;
crate::write_if_changed(
dest,
generated_lock(contents, &skeleton_package, package_name)?.as_bytes(),
)?;
}
Ok(())
}
#[cfg(feature = "external-skeleton")]
pub fn copy_skeleton_lock(output: &Utf8Path, package_name: &str) -> anyhow::Result<()> {
let source = skeleton_root().join("Cargo.lock");
match std::fs::read_to_string(&source) {
Ok(contents) => {
let skeleton_package = skeleton_package_name()?;
crate::write_if_changed(
output.join("Cargo.lock"),
generated_lock(&contents, &skeleton_package, package_name)?.as_bytes(),
)?
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(anyhow!(
"Failed to read skeleton lockfile {source}: {error}"
));
}
}
Ok(())
}
fn skeleton_package_name() -> anyhow::Result<String> {
let manifest = skeleton_cargo_toml()?
.parse::<DocumentMut>()
.map_err(|error| anyhow!("Failed to parse skeleton Cargo.toml: {error}"))?;
manifest["package"]["name"]
.as_str()
.map(ToOwned::to_owned)
.ok_or_else(|| anyhow!("Missing package.name in skeleton Cargo.toml"))
}
fn generated_lock(
contents: &str,
skeleton_package_name: &str,
package_name: &str,
) -> anyhow::Result<String> {
let skeleton_package = format!("name = \"{skeleton_package_name}\"");
let matches = contents.match_indices(&skeleton_package).count();
if matches != 1 {
return Err(anyhow!(
"Expected exactly one {skeleton_package:?} entry in the skeleton lockfile, found {matches}"
));
}
Ok(contents.replacen(&skeleton_package, &format!("name = \"{package_name}\""), 1))
}
#[cfg(not(feature = "external-skeleton"))]
pub fn copy_skeleton_sources(output: &Utf8Path) -> anyhow::Result<()> {
if let Some(src) = SKELETON.get_dir("src") {
for file in src.files() {
let src_path = Utf8Path::from_path(file.path())
.ok_or_else(|| anyhow!("Unexpected non-UTF-8 path in skeleton"))?;
if GENERATED_FILES.contains(&src_path.as_str()) {
continue;
}
let dest_path = output.join(src_path);
crate::write_if_changed(dest_path, file.contents())?;
}
for dir in src.dirs() {
recursive_copy_sources(dir, output)?;
}
}
Ok(())
}
#[cfg(not(feature = "external-skeleton"))]
fn recursive_copy_sources(dir: &Dir, output: &Utf8Path) -> anyhow::Result<()> {
let dir_path = Utf8Path::from_path(dir.path())
.ok_or_else(|| anyhow!("Unexpected non-UTF-8 path in skeleton"))?;
std::fs::create_dir_all(output.join(dir_path))?;
let has_mod_rs = dir
.files()
.any(|f| f.path().file_name().and_then(|n| n.to_str()) == Some("mod.rs"));
if has_mod_rs {
let stale = output.join(format!("{dir_path}.rs"));
if stale.exists() {
std::fs::remove_file(&stale)
.map_err(|e| anyhow!("Failed to remove stale module file {stale}: {e}"))?;
}
}
for file in dir.files() {
let src_path = Utf8Path::from_path(file.path())
.ok_or_else(|| anyhow!("Unexpected non-UTF-8 path in skeleton"))?;
let dest_path = output.join(src_path);
crate::write_if_changed(dest_path, file.contents())?;
}
for dir in dir.dirs() {
recursive_copy_sources(dir, output)?;
}
Ok(())
}
#[cfg(feature = "external-skeleton")]
pub fn copy_skeleton_sources(output: &Utf8Path) -> anyhow::Result<()> {
let root = skeleton_root();
recursive_copy_external_sources(&root.join("src"), &root, output, false)
}
#[cfg(feature = "external-skeleton")]
fn recursive_copy_external_sources(
source: &Utf8Path,
root: &Utf8Path,
output: &Utf8Path,
remove_stale_sibling: bool,
) -> anyhow::Result<()> {
let relative = source
.strip_prefix(root)
.map_err(|error| anyhow!("Invalid skeleton source path {source}: {error}"))?;
std::fs::create_dir_all(output.join(relative))?;
let mut entries = std::fs::read_dir(source)
.map_err(|error| anyhow!("Failed to read skeleton directory {source}: {error}"))?
.collect::<Result<Vec<_>, _>>()?;
entries.sort_by_key(|entry| entry.file_name());
if remove_stale_sibling
&& entries.iter().any(|entry| {
entry.file_name() == "mod.rs"
&& entry
.file_type()
.map(|file_type| file_type.is_file())
.unwrap_or(false)
})
{
let stale = output.join(format!("{relative}.rs"));
if stale.exists() {
std::fs::remove_file(&stale)
.map_err(|error| anyhow!("Failed to remove stale module file {stale}: {error}"))?;
}
}
for entry in entries {
let path = Utf8PathBuf::from_path_buf(entry.path())
.map_err(|path| anyhow!("Unexpected non-UTF-8 path in skeleton: {}", path.display()))?;
let file_type = entry
.file_type()
.map_err(|error| anyhow!("Failed to inspect skeleton path {path}: {error}"))?;
if file_type.is_dir() {
recursive_copy_external_sources(&path, root, output, true)?;
} else if file_type.is_file() {
let relative = path
.strip_prefix(root)
.map_err(|error| anyhow!("Invalid skeleton source path {path}: {error}"))?;
if !GENERATED_FILES.contains(&relative.as_str()) {
crate::write_if_changed(output.join(relative), std::fs::read(&path)?)?;
}
}
}
Ok(())
}
#[cfg(test)]
mod module_loader_architecture;
#[cfg(test)]
mod tests {
use super::generated_lock;
#[test]
fn generated_lock_requires_exactly_one_skeleton_package() {
let lock = "[[package]]\nname = \"rquickjs-component\"\nversion = \"0.0.0\"\n";
assert_eq!(
generated_lock(lock, "rquickjs-component", "generated-world").unwrap(),
"[[package]]\nname = \"generated-world\"\nversion = \"0.0.0\"\n"
);
assert!(
generated_lock(
"[[package]]\nname = \"other\"\n",
"rquickjs-component",
"generated-world"
)
.is_err()
);
assert!(
generated_lock(
&format!("{lock}{lock}"),
"rquickjs-component",
"generated-world"
)
.is_err()
);
}
}