use anyhow::{bail, Result};
use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};
pub fn render(src: &str, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
let parsed = parse(src)?;
match parsed.ecosystem.as_str() {
"rust-single-crate" => render_rust_single(&parsed, out, force),
"rust-workspace" => render_rust_workspace(&parsed, out, force),
"npm" => render_npm(&parsed, out, force),
"python" => render_python(&parsed, out, force),
"helm" => render_helm(&parsed, out, force),
"github-action" => render_github_action(&parsed, out, force),
"go" => render_go(&parsed, out, force),
"crystal" => render_crystal(&parsed, out, force),
"dart" => render_dart(&parsed, out, force),
"composer" => render_composer(&parsed, out, force),
"julia" => render_julia(&parsed, out, force),
"java-maven" => render_maven(&parsed, out, force),
"dotnet-csproj" => render_csproj(&parsed, out, force),
"ocaml-dune" => render_dune(&parsed, out, force),
"java-gradle-kts" => render_gradle_kts(&parsed, out, force),
"swift-spm" => render_swift_spm(&parsed, out, force),
"elixir-mix" => render_elixir_mix(&parsed, out, force),
"ruby-gem" => render_ruby_gem(&parsed, out, force),
"zig" => render_zig(&parsed, out, force),
"nim-nimble" => render_nim_nimble(&parsed, out, force),
"scala-sbt" => render_sbt(&parsed, out, force),
"clojure-deps" => render_clj_deps(&parsed, out, force),
"r-description" => render_r(&parsed, out, force),
"lua-rockspec" => render_rockspec(&parsed, out, force),
"cpp-conan" => render_conan(&parsed, out, force),
"python-conda" => render_conda(&parsed, out, force),
"python-pipenv" => render_pipenv(&parsed, out, force),
"python-pdm" => render_pdm(&parsed, out, force),
"js-deno" => render_deno(&parsed, out, force),
"js-pnpm" => render_pnpm(&parsed, out, force),
"cpp-vcpkg" => render_vcpkg(&parsed, out, force),
"cpp-meson" => render_meson(&parsed, out, force),
"fortran-fpm" => render_fpm(&parsed, out, force),
"gleam" => render_gleam(&parsed, out, force),
"ada-alire" => render_alire(&parsed, out, force),
"haskell-cabal" => render_cabal(&parsed, out, force),
"racket-info" => render_racket(&parsed, out, force),
other => bail!(
"ecosystem '{other}' not supported. 33 supported: \
rust-single-crate, rust-workspace, npm, python, helm, \
github-action, go, crystal, dart, composer, julia, \
java-maven, dotnet-csproj, ocaml-dune, java-gradle-kts, \
swift-spm, elixir-mix, ruby-gem, zig, nim-nimble, \
scala-sbt, clojure-deps, r-description, lua-rockspec, cpp-conan, \
python-conda, python-pipenv, python-pdm, js-deno, js-pnpm, \
cpp-vcpkg, cpp-meson, fortran-fpm, gleam, ada-alire, \
haskell-cabal, racket-info."
),
}
}
fn render_go(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::lined_ast::{render as render_lined, Line};
let mut written = vec![];
fs::create_dir_all(out)?;
let gomod = out.join("go.mod");
if !gomod.exists() || force {
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let module_path = pkg.get("repository").cloned()
.map(|r| r.trim_start_matches("https://").trim_start_matches("http://").to_string())
.unwrap_or_else(|| {
let mut p = String::from("github.com/pleme-io/");
p.push_str(&name);
p
});
let go_ver = c.supports_raw.as_deref()
.and_then(|r| read_keyword_in_block(r, ":go"))
.map(|v| v.trim_matches('"').trim_start_matches(">=").to_string())
.unwrap_or_else(|| "1.22".to_string());
let mut lines: Vec<Line> = vec![
Line::Directive { keyword: "module".into(), args: vec![module_path] },
Line::Blank,
Line::Directive { keyword: "go".into(), args: vec![go_ver] },
];
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
if !deps.runtime.is_empty() {
let inner: Vec<Line> = deps.runtime.iter()
.filter_map(|d| {
let n = d.get("name")?;
let v = d.get("version")?;
Some(Line::Directive {
keyword: n.clone(),
args: vec![v.clone()],
})
})
.collect();
lines.push(Line::Blank);
lines.push(Line::Group { keyword: "require".into(), inner });
}
crate::ast::emit(&gomod, &crate::lined_ast::Lines(lines))?;
written.push(gomod);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_crystal(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::yaml_ast::{render as render_yaml, Value as YVal};
let mut written = vec![];
fs::create_dir_all(out)?;
let shard = out.join("shard.yml");
if !shard.exists() || force {
let pkg = &c.package;
let mut m = YVal::map();
if let Some(n) = pkg.get("name") { m.insert("name", YVal::s(n)); }
if let Some(v) = pkg.get("version") { m.insert("version", YVal::s(v)); }
if let Some(l) = pkg.get("license") { m.insert("license", YVal::s(l)); }
if let Some(authors) = pkg.get("authors") {
m.insert("authors", YVal::arr([YVal::s(authors)]));
}
if let Some(supports) = c.supports_raw.as_deref() {
if let Some(cr) = read_keyword_in_block(supports, ":crystal") {
m.insert("crystal", YVal::s(cr.trim_matches('"')));
}
}
fs::write(&shard, render_yaml(&m))?;
written.push(shard);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_dart(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::yaml_ast::{render as render_yaml, Value as YVal};
let mut written = vec![];
fs::create_dir_all(out)?;
let pubspec = out.join("pubspec.yaml");
if !pubspec.exists() || force {
let pkg = &c.package;
let mut m = YVal::map();
if let Some(n) = pkg.get("name") { m.insert("name", YVal::s(n)); }
if let Some(v) = pkg.get("version") { m.insert("version", YVal::s(v)); }
if let Some(d) = pkg.get("description") { m.insert("description", YVal::s(d)); }
if let Some(r) = pkg.get("repository") { m.insert("repository", YVal::s(r)); }
if let Some(supports) = c.supports_raw.as_deref() {
if let Some(sdk) = read_keyword_in_block(supports, ":dart") {
let mut env = YVal::map();
env.insert("sdk", YVal::s(sdk.trim_matches('"')));
m.insert("environment", env);
}
}
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
if !deps.runtime.is_empty() {
let mut rt = YVal::map();
for d in &deps.runtime {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
rt.insert(n, YVal::s(v));
}
}
m.insert("dependencies", rt);
}
if !deps.dev.is_empty() {
let mut dv = YVal::map();
for d in &deps.dev {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
dv.insert(n, YVal::s(v));
}
}
m.insert("dev_dependencies", dv);
}
fs::write(&pubspec, render_yaml(&m))?;
written.push(pubspec);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_composer(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::json_ast::{render as render_json, Value as JVal};
let mut written = vec![];
fs::create_dir_all(out)?;
let composer = out.join("composer.json");
if !composer.exists() || force {
let pkg = &c.package;
let mut root = JVal::obj();
if let Some(n) = pkg.get("name") { root.insert("name", JVal::s(n)); }
if let Some(d) = pkg.get("description") { root.insert("description", JVal::s(d)); }
root.insert("type", JVal::s("library"));
if let Some(l) = pkg.get("license") { root.insert("license", JVal::s(l)); }
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
let php_req = c.supports_raw.as_deref()
.and_then(|r| read_keyword_in_block(r, ":php"))
.map(|v| v.trim_matches('"').to_string())
.unwrap_or_else(|| ">=8.2".to_string());
let mut require = JVal::obj();
require.insert("php", JVal::s(php_req));
for d in &deps.runtime {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
require.insert(n, JVal::s(v));
}
}
root.insert("require", require);
if !deps.dev.is_empty() {
let mut dev = JVal::obj();
for d in &deps.dev {
if let Some(n) = d.get("name") {
let v = d.get("version").cloned().unwrap_or_else(|| "*".to_string());
dev.insert(n, JVal::s(v));
}
}
root.insert("require-dev", dev);
}
fs::write(&composer, render_json(&root))?;
written.push(composer);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&").replace('<', "<").replace('>', ">")
}
fn render_maven(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::xml_ast::{render_document, Element};
let mut written = vec![];
fs::create_dir_all(out)?;
let pom = out.join("pom.xml");
if !pom.exists() || force {
let pkg = &c.package;
let group = pkg.get("group-id").cloned().unwrap_or_else(|| "io.pleme".to_string());
let artifact = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let mut project = Element::new("project")
.attr("xmlns", "http://maven.apache.org/POM/4.0.0")
.attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
.attr("xsi:schemaLocation", "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd")
.child(Element::new("modelVersion").text("4.0.0"))
.child(Element::new("groupId").text(group))
.child(Element::new("artifactId").text(artifact))
.child(Element::new("version").text(version))
.child(Element::new("packaging").text("jar"));
if let Some(d) = pkg.get("description") {
project.push(Element::new("description").text(d));
}
if let Some(l) = pkg.get("license") {
let lic = Element::new("license").child(Element::new("name").text(l));
project.push(Element::new("licenses").child(lic));
}
if let Some(r) = pkg.get("repository") {
project.push(Element::new("scm").child(Element::new("url").text(r)));
}
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
if !deps.runtime.is_empty() || !deps.dev.is_empty() {
let mut dependencies = Element::new("dependencies");
for d in &deps.runtime {
if let Some(n) = d.get("name") {
let v = d.get("version").cloned().unwrap_or_default();
let g = d.get("group-id").cloned().unwrap_or_else(|| "org.example".to_string());
let mut dep = Element::new("dependency")
.child(Element::new("groupId").text(g))
.child(Element::new("artifactId").text(n));
if !v.is_empty() {
dep.push(Element::new("version").text(v));
}
dependencies.push(dep);
}
}
for d in &deps.dev {
if let Some(n) = d.get("name") {
let v = d.get("version").cloned().unwrap_or_default();
let g = d.get("group-id").cloned().unwrap_or_else(|| "org.example".to_string());
let mut dep = Element::new("dependency")
.child(Element::new("groupId").text(g))
.child(Element::new("artifactId").text(n));
if !v.is_empty() {
dep.push(Element::new("version").text(v));
}
dep.push(Element::new("scope").text("test"));
dependencies.push(dep);
}
}
project.push(dependencies);
}
crate::ast::emit(&pom, &project)?;
written.push(pom);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_csproj(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::xml_ast::Element;
let mut written = vec![];
fs::create_dir_all(out)?;
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let path = out.join(format!("{name}.csproj"));
if !path.exists() || force {
let target = c.supports_raw.as_deref()
.and_then(|r| read_keyword_in_block(r, ":dotnet"))
.map(|v| v.trim_matches('"').to_string())
.unwrap_or_else(|| "net8.0".to_string());
let output_type = if c.kind == "Binario" { "Exe" } else { "Library" };
let mut props = Element::new("PropertyGroup")
.child(Element::new("TargetFramework").text(target))
.child(Element::new("OutputType").text(output_type));
if let Some(v) = pkg.get("version") { props.push(Element::new("Version").text(v)); }
if let Some(a) = pkg.get("authors") { props.push(Element::new("Authors").text(a)); }
if let Some(d) = pkg.get("description") { props.push(Element::new("Description").text(d)); }
if let Some(l) = pkg.get("license") { props.push(Element::new("PackageLicenseExpression").text(l)); }
if let Some(r) = pkg.get("repository") { props.push(Element::new("RepositoryUrl").text(r)); }
let mut project = Element::new("Project")
.attr("Sdk", "Microsoft.NET.Sdk")
.child(props);
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
if !deps.runtime.is_empty() || !deps.dev.is_empty() {
let mut items = Element::new("ItemGroup");
for d in &deps.runtime {
if let Some(n) = d.get("name") {
let v = d.get("version").cloned().unwrap_or_default();
items.push(Element::new("PackageReference").attr("Include", n).attr("Version", v));
}
}
for d in &deps.dev {
if let Some(n) = d.get("name") {
let v = d.get("version").cloned().unwrap_or_default();
items.push(
Element::new("PackageReference")
.attr("Include", n).attr("Version", v).attr("PrivateAssets", "all"),
);
}
}
project.push(items);
}
let mut out_str = String::new();
crate::xml_ast::render_doc_no_decl(&project, &mut out_str);
fs::write(&path, out_str)?;
written.push(path);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_dune(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::sexp_ast::{render_forms, SExp};
let mut written = vec![];
fs::create_dir_all(out)?;
let dune = out.join("dune-project");
if !dune.exists() || force {
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let mut forms: Vec<SExp> = vec![
SExp::list([SExp::sym("lang"), SExp::sym("dune"), SExp::sym("3.14")]),
SExp::list([SExp::sym("name"), SExp::sym(&name)]),
];
if let Some(v) = pkg.get("version") {
forms.push(SExp::list([SExp::sym("version"), SExp::sym(v)]));
}
forms.push(SExp::list([SExp::sym("generate_opam_files"), SExp::sym("true")]));
if let Some(r) = pkg.get("repository") {
if r.contains("github.com") {
let gh = r.trim_start_matches("https://github.com/").trim_end_matches(".git");
forms.push(SExp::list([
SExp::sym("source"),
SExp::list([SExp::sym("github"), SExp::sym(gh)]),
]));
}
}
if let Some(l) = pkg.get("license") {
forms.push(SExp::list([SExp::sym("license"), SExp::sym(l)]));
}
forms.push(SExp::list([SExp::sym("authors"), SExp::str("pleme-io")]));
forms.push(SExp::list([SExp::sym("maintainers"), SExp::str("engineering@pleme.io")]));
let ocaml_req = c.supports_raw.as_deref()
.and_then(|r| read_keyword_in_block(r, ":ocaml"))
.map(|v| v.trim_matches('"').trim_start_matches(">=").to_string())
.unwrap_or_else(|| "4.14".to_string());
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
let mut depends: Vec<SExp> = vec![
SExp::sym("depends"),
SExp::list([
SExp::sym("ocaml"),
SExp::list([SExp::sym(">="), SExp::sym(&ocaml_req)]),
]),
SExp::sym("dune"),
];
for d in &deps.runtime {
if let Some(n) = d.get("name") { depends.push(SExp::sym(n)); }
}
for d in &deps.dev {
if let Some(n) = d.get("name") {
depends.push(SExp::list([SExp::sym(n), SExp::kw("with-test")]));
}
}
let mut pkg_form: Vec<SExp> = vec![
SExp::sym("package"),
SExp::list([SExp::sym("name"), SExp::sym(&name)]),
];
if let Some(d) = pkg.get("description") {
pkg_form.push(SExp::list([SExp::sym("synopsis"), SExp::str(d)]));
}
pkg_form.push(SExp::List(depends));
forms.push(SExp::List(pkg_form));
crate::ast::emit(&dune, &crate::sexp_ast::Forms(forms))?;
written.push(dune);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_gradle_kts(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
let mut written = vec![];
fs::create_dir_all(out)?;
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let group = pkg.get("group-id").cloned().unwrap_or_else(|| "io.pleme".to_string());
let java_ver = c.supports_raw.as_deref()
.and_then(|r| read_keyword_in_block(r, ":java"))
.map(|v| v.trim_matches('"').trim_start_matches(">=").to_string())
.unwrap_or_else(|| "17".to_string());
let settings = out.join("settings.gradle.kts");
if !settings.exists() || force {
fs::write(&settings, format!("rootProject.name = \"{name}\"\n"))?;
written.push(settings);
}
let build = out.join("build.gradle.kts");
if !build.exists() || force {
let is_app = c.kind == "Binario";
let plugin = if is_app { "application" } else { "`java-library`" };
let mut s = format!("plugins {{ {plugin} }}\n\n");
s.push_str(&format!("group = \"{group}\"\nversion = \"{version}\"\n\n"));
s.push_str("repositories { mavenCentral() }\n\n");
s.push_str("dependencies {\n");
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
for d in &deps.runtime {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
let g = d.get("group-id").cloned().unwrap_or_else(|| "com.example".to_string());
s.push_str(&format!(" implementation(\"{g}:{n}:{v}\")\n"));
}
}
for d in &deps.dev {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
let g = d.get("group-id").cloned().unwrap_or_else(|| "com.example".to_string());
s.push_str(&format!(" testImplementation(\"{g}:{n}:{v}\")\n"));
}
}
s.push_str("}\n\n");
s.push_str(&format!("java {{ toolchain {{ languageVersion = JavaLanguageVersion.of({java_ver}) }} }}\n"));
fs::write(&build, s)?;
written.push(build);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_swift_spm(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
let mut written = vec![];
fs::create_dir_all(out)?;
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let pkg_swift = out.join("Package.swift");
if !pkg_swift.exists() || force {
let tools_ver = c.supports_raw.as_deref()
.and_then(|r| read_keyword_in_block(r, ":swift-tools"))
.map(|v| v.trim_matches('"').to_string())
.unwrap_or_else(|| "5.9".to_string());
let is_exe = c.kind == "Binario";
let mut s = format!("// swift-tools-version:{tools_ver}\nimport PackageDescription\n\nlet package = Package(\n name: \"{name}\",\n");
s.push_str(" platforms: [.macOS(.v13)],\n");
if is_exe {
s.push_str(&format!(" products: [.executable(name: \"{name}\", targets: [\"{name}\"])],\n"));
} else {
s.push_str(&format!(" products: [.library(name: \"{name}\", targets: [\"{name}\"])],\n"));
}
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
if !deps.runtime.is_empty() {
s.push_str(" dependencies: [\n");
for d in &deps.runtime {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
let url = d.get("repository").cloned()
.unwrap_or_else(|| format!("https://github.com/{n}/{n}"));
s.push_str(&format!(" .package(url: \"{url}\", from: \"{v}\"),\n"));
}
}
s.push_str(" ],\n");
}
let target_kind = if is_exe { "executableTarget" } else { "target" };
s.push_str(&format!(" targets: [\n .{target_kind}(name: \"{name}\"),\n ]\n)\n"));
fs::write(&pkg_swift, s)?;
written.push(pkg_swift);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_elixir_mix(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
let mut written = vec![];
fs::create_dir_all(out)?;
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let mix = out.join("mix.exs");
if !mix.exists() || force {
let module = format!("{}.MixProject", name.replace('-', "_")
.split('_').map(|p| {
let mut chars = p.chars();
match chars.next() {
Some(c) => c.to_uppercase().chain(chars).collect(),
None => String::new(),
}
}).collect::<String>());
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let elixir_req = c.supports_raw.as_deref()
.and_then(|r| read_keyword_in_block(r, ":elixir"))
.map(|v| v.trim_matches('"').to_string())
.unwrap_or_else(|| "~> 1.16".to_string());
let desc = pkg.get("description").cloned().unwrap_or_default();
let license = pkg.get("license").cloned().unwrap_or_else(|| "MIT".to_string());
let mut s = format!("defmodule {module} do\n use Mix.Project\n\n def project, do: [\n");
s.push_str(&format!(" app: :{},\n", name.replace('-', "_")));
s.push_str(&format!(" version: \"{version}\",\n"));
s.push_str(&format!(" elixir: \"{elixir_req}\",\n"));
s.push_str(&format!(" description: \"{desc}\",\n"));
s.push_str(&format!(" package: [licenses: [\"{license}\"]],\n"));
s.push_str(" deps: deps()\n ]\n\n");
s.push_str(" def application, do: [extra_applications: [:logger]]\n\n");
s.push_str(" defp deps, do: [\n");
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
for d in &deps.runtime {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
s.push_str(&format!(" {{:{n}, \"{v}\"}},\n"));
}
}
for d in &deps.dev {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
s.push_str(&format!(" {{:{n}, \"{v}\", only: :dev, runtime: false}},\n"));
}
}
s.push_str(" ]\nend\n");
fs::write(&mix, s)?;
written.push(mix);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_ruby_gem(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::ast::Render;
use crate::ruby_ast::{Block, Expr, Stmt};
let mut written = vec![];
fs::create_dir_all(out)?;
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let gemspec = out.join(format!("{name}.gemspec"));
if !gemspec.exists() || force {
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let desc = pkg.get("description").cloned().unwrap_or_default();
let license = pkg.get("license").cloned().unwrap_or_else(|| "MIT".to_string());
let homepage = pkg.get("homepage").or_else(|| pkg.get("repository"))
.cloned()
.unwrap_or_else(|| {
let mut h = String::from("https://github.com/pleme-io/");
h.push_str(&name);
h
});
let ruby_req = c.supports_raw.as_deref()
.and_then(|r| read_keyword_in_block(r, ":ruby"))
.map(|v| v.trim_matches('"').to_string())
.unwrap_or_else(|| ">= 3.0".to_string());
let assign = |attr: &str, value: Expr| Stmt::Assign {
receiver: "spec".to_string(),
attr: attr.to_string(),
value,
};
let method = |m: &str, args: Vec<Expr>| Stmt::MethodCall {
receiver: "spec".to_string(),
method: m.to_string(),
args,
};
let mut block = Block::new("Gem::Specification.new", "spec");
block.push(assign("name", Expr::s(&name)));
block.push(assign("version", Expr::s(version)));
block.push(assign("authors", Expr::arr([Expr::s("pleme-io")])));
block.push(assign("email", Expr::arr([Expr::s("engineering@pleme.io")])));
block.push(assign("summary", Expr::s(&desc)));
block.push(assign("description", Expr::s(&desc)));
block.push(assign("license", Expr::s(license)));
block.push(assign("homepage", Expr::s(homepage)));
block.push(assign("files", Expr::raw("Dir['lib/**/*.rb']")));
block.push(assign("required_ruby_version", Expr::s(ruby_req)));
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
for d in &deps.runtime {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
block.push(method("add_dependency", vec![Expr::s(n), Expr::s(v)]));
}
}
for d in &deps.dev {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
block.push(method("add_development_dependency",
vec![Expr::s(n), Expr::s(v)]));
}
}
fs::write(&gemspec, block.render())?;
written.push(gemspec);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_zig(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
let mut written = vec![];
fs::create_dir_all(out)?;
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let min_zig = c.supports_raw.as_deref()
.and_then(|r| read_keyword_in_block(r, ":zig"))
.map(|v| v.trim_matches('"').to_string())
.unwrap_or_else(|| "0.14.0".to_string());
let zon = out.join("build.zig.zon");
if !zon.exists() || force {
let mut s = format!(".{{\n .name = .{},\n .version = \"{version}\",\n .minimum_zig_version = \"{min_zig}\",\n",
name.replace('-', "_"));
s.push_str(" .dependencies = .{},\n");
s.push_str(" .paths = .{ \"build.zig\", \"build.zig.zon\", \"src\" },\n}\n");
fs::write(&zon, s)?;
written.push(zon);
}
let build = out.join("build.zig");
if !build.exists() || force {
let is_exe = c.kind == "Binario";
let kind_fn = if is_exe { "addExecutable" } else { "addLibrary" };
let mut s = String::from("const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n");
s.push_str(" const target = b.standardTargetOptions(.{});\n");
s.push_str(" const optimize = b.standardOptimizeOption(.{});\n");
s.push_str(&format!(" const artifact = b.{kind_fn}(.{{\n"));
s.push_str(&format!(" .name = \"{name}\",\n"));
s.push_str(" .root_source_file = b.path(\"src/main.zig\"),\n");
s.push_str(" .target = target,\n .optimize = optimize,\n });\n");
s.push_str(" b.installArtifact(artifact);\n}\n");
fs::write(&build, s)?;
written.push(build);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_nim_nimble(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::lined_ast::{render as render_lined, Line};
use crate::toml_ast::Value as TVal;
let mut written = vec![];
fs::create_dir_all(out)?;
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let nimble = out.join(format!("{name}.nimble"));
if !nimble.exists() || force {
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let desc = pkg.get("description").cloned().unwrap_or_default();
let license = pkg.get("license").cloned().unwrap_or_else(|| "MIT".to_string());
let nim_req = c.supports_raw.as_deref()
.and_then(|r| read_keyword_in_block(r, ":nim"))
.map(|v| v.trim_matches('"').trim_start_matches(">=").trim().to_string())
.unwrap_or_else(|| "2.0.0".to_string());
let mut lines: Vec<Line> = vec![
assign("version", &version),
assign("author", "pleme-io"),
assign("description", &desc),
assign("license", &license),
assign("srcDir", "src"),
];
if c.kind == "Binario" {
let arr_lit = TVal::arr([TVal::s(&name)]).render_inline();
let mut nim_array = String::from("@");
nim_array.push_str(&arr_lit);
lines.push(Line::Directive {
keyword: "bin".to_string(),
args: vec!["=".to_string(), nim_array],
});
}
lines.push(Line::Blank);
lines.push(requires_line("nim", &nim_req));
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
for d in &deps.runtime {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
lines.push(requires_line(n, v));
}
}
fs::write(&nimble, render_lined(&lines))?;
written.push(nimble);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn assign(key: &str, value: &str) -> crate::lined_ast::Line {
use crate::lined_ast::Line;
use crate::toml_ast::Value as TVal;
Line::Directive {
keyword: key.to_string(),
args: vec!["=".to_string(), TVal::s(value).render_inline()],
}
}
fn requires_line(name: &str, ver: &str) -> crate::lined_ast::Line {
use crate::lined_ast::Line;
use crate::toml_ast::Value as TVal;
let mut constraint = String::from(name);
constraint.push_str(" >= ");
constraint.push_str(ver);
Line::Directive {
keyword: "requires".to_string(),
args: vec![TVal::s(constraint).render_inline()],
}
}
fn render_sbt(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
let mut written = vec![];
fs::create_dir_all(out)?;
let pkg = &c.package;
let build = out.join("build.sbt");
if !build.exists() || force {
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let scala_ver = c.supports_raw.as_deref()
.and_then(|r| read_keyword_in_block(r, ":scala"))
.map(|v| v.trim_matches('"').to_string())
.unwrap_or_else(|| "3.4.0".to_string());
let mut s = format!("name := \"{name}\"\nversion := \"{version}\"\nscalaVersion := \"{scala_ver}\"\n");
if let Some(d) = pkg.get("description") {
s.push_str(&format!("description := \"{}\"\n", d.replace('"', "\\\"")));
}
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
if !deps.runtime.is_empty() {
s.push_str("\nlibraryDependencies ++= Seq(\n");
for d in &deps.runtime {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
let g = d.get("group-id").cloned().unwrap_or_else(|| "org.example".to_string());
s.push_str(&format!(" \"{g}\" %% \"{n}\" % \"{v}\",\n"));
}
}
s.push_str(")\n");
}
fs::write(&build, s)?;
written.push(build);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_clj_deps(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::sexp_ast::{render_forms, SExp};
let mut written = vec![];
fs::create_dir_all(out)?;
let deps_edn = out.join("deps.edn");
if !deps_edn.exists() || force {
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
let mut deps_map: Vec<(SExp, SExp)> = vec![];
for d in &deps.runtime {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
let inner = SExp::map_([(SExp::kw("mvn/version"), SExp::str(v))]);
deps_map.push((SExp::sym(n), inner));
}
}
let root = SExp::map_([
(SExp::kw("paths"), SExp::vec_([SExp::str("src"), SExp::str("resources")])),
(SExp::kw("deps"), SExp::Map(deps_map)),
]);
fs::write(&deps_edn, render_forms(&[root]))?;
written.push(deps_edn);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_r(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::control_ast::{render as render_ctrl, Line};
let mut written = vec![];
fs::create_dir_all(out)?;
let desc = out.join("DESCRIPTION");
if !desc.exists() || force {
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let mut lines: Vec<Line> = vec![
Line::Field { key: "Package".into(), value: name },
Line::Field { key: "Version".into(), value: version },
Line::Field { key: "Type".into(), value: "Package".into() },
];
if let Some(d) = pkg.get("description") {
lines.push(Line::Field { key: "Title".into(), value: d.clone() });
lines.push(Line::Field { key: "Description".into(), value: d.clone() });
}
if let Some(l) = pkg.get("license") {
lines.push(Line::Field { key: "License".into(), value: l.clone() });
}
lines.push(Line::Field {
key: "Authors@R".into(),
value: r#"person("pleme-io", role = c("aut", "cre"))"#.into(),
});
lines.push(Line::Field { key: "Encoding".into(), value: "UTF-8".into() });
lines.push(Line::Field { key: "RoxygenNote".into(), value: "7.3.1".into() });
crate::ast::emit(&desc, &crate::control_ast::Document::from(1, lines))?;
written.push(desc);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_rockspec(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
let mut written = vec![];
fs::create_dir_all(out)?;
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let rockspec = out.join(format!("{name}-{version}-1.rockspec"));
if !rockspec.exists() || force {
let mut s = format!("package = \"{name}\"\nversion = \"{version}-1\"\n");
s.push_str("source = { url = \"\" }\n");
s.push_str("description = {\n");
if let Some(d) = pkg.get("description") { s.push_str(&format!(" summary = \"{d}\",\n")); }
if let Some(l) = pkg.get("license") { s.push_str(&format!(" license = \"{l}\"\n")); }
s.push_str("}\ndependencies = { \"lua >= 5.1\" }\n");
s.push_str("build = { type = \"builtin\", modules = {} }\n");
fs::write(&rockspec, s)?;
written.push(rockspec);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_conan(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
let mut written = vec![];
fs::create_dir_all(out)?;
let conanfile = out.join("conanfile.py");
if !conanfile.exists() || force {
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let class_name = name.replace('-', "_").split('_').map(|p| {
let mut chars = p.chars();
chars.next().map(|c| c.to_uppercase().chain(chars).collect::<String>()).unwrap_or_default()
}).collect::<String>();
let mut s = String::from("from conan import ConanFile\n\n");
s.push_str(&format!("class {class_name}Conan(ConanFile):\n"));
s.push_str(&format!(" name = \"{name}\"\n version = \"{version}\"\n"));
if let Some(l) = pkg.get("license") { s.push_str(&format!(" license = \"{l}\"\n")); }
s.push_str(" settings = \"os\", \"compiler\", \"build_type\", \"arch\"\n");
fs::write(&conanfile, s)?;
written.push(conanfile);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_conda(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::yaml_ast::{render as render_yaml, Value as YVal};
let mut written = vec![];
fs::create_dir_all(out)?;
let meta = out.join("meta.yaml");
if !meta.exists() || force {
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let mut root = YVal::map();
let mut package = YVal::map();
package.insert("name", YVal::s(&name));
package.insert("version", YVal::s(version));
root.insert("package", package);
let mut source = YVal::map();
source.insert("path", YVal::s("."));
root.insert("source", source);
let mut build = YVal::map();
build.insert("noarch", YVal::s("python"));
build.insert("script", YVal::s("pip install . --no-deps"));
root.insert("build", build);
let mut requirements = YVal::map();
requirements.insert("host", YVal::arr([YVal::s("python"), YVal::s("pip")]));
requirements.insert("run", YVal::arr([YVal::s("python")]));
root.insert("requirements", requirements);
if let Some(d) = pkg.get("description") {
let mut about = YVal::map();
about.insert("summary", YVal::s(d));
root.insert("about", about);
}
fs::write(&meta, render_yaml(&root))?;
written.push(meta);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_pipenv(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::toml_ast::{Document, Value as TVal};
let mut written = vec![];
fs::create_dir_all(out)?;
let pipfile = out.join("Pipfile");
if !pipfile.exists() || force {
let mut doc = Document::new();
let source = doc.array_table("source");
source.key("name", TVal::s("pypi"));
source.key("url", TVal::s("https://pypi.org/simple"));
source.key("verify_ssl", TVal::b(true));
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
let packages = doc.table("packages");
for d in &deps.runtime {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
packages.key(n.clone(), TVal::s(v));
}
}
let dev_packages = doc.table("dev-packages");
for d in &deps.dev {
if let (Some(n), Some(v)) = (d.get("name"), d.get("version")) {
dev_packages.key(n.clone(), TVal::s(v));
}
}
fs::write(&pipfile, doc.render())?;
written.push(pipfile);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_pdm(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
render_python(c, out, force)
}
fn render_deno(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::json_ast::{render as render_json, Value as JVal};
let mut written = vec![];
fs::create_dir_all(out)?;
let deno_json = out.join("deno.json");
if !deno_json.exists() || force {
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let mut root = JVal::obj();
let mut scoped = String::from("@pleme-io/");
scoped.push_str(&name);
root.insert("name", JVal::s(scoped));
root.insert("version", JVal::s(version));
root.insert("exports", JVal::s("./mod.ts"));
let mut tasks = JVal::obj();
tasks.insert("test", JVal::s("deno test"));
root.insert("tasks", tasks);
root.insert("imports", JVal::obj());
fs::write(&deno_json, render_json(&root))?;
written.push(deno_json);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_pnpm(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::yaml_ast::{render as render_yaml, Value as YVal};
let mut written = render_npm(c, out, force)?;
let pnpm_ws = out.join("pnpm-workspace.yaml");
if !pnpm_ws.exists() || force {
let mut root = YVal::map();
root.insert("packages", YVal::arr([YVal::s("packages/*")]));
fs::write(&pnpm_ws, render_yaml(&root))?;
written.push(pnpm_ws);
}
Ok(written)
}
fn render_vcpkg(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::json_ast::{render as render_json, Value as JVal};
let mut written = vec![];
fs::create_dir_all(out)?;
let vcpkg = out.join("vcpkg.json");
if !vcpkg.exists() || force {
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let mut root = JVal::obj();
root.insert("name", JVal::s(name));
root.insert("version-string", JVal::s(version));
if let Some(d) = pkg.get("description") { root.insert("description", JVal::s(d)); }
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
let names: Vec<JVal> = deps.runtime.iter()
.filter_map(|d| d.get("name").cloned())
.map(JVal::s)
.collect();
root.insert("dependencies", JVal::Array(names));
fs::write(&vcpkg, render_json(&root))?;
written.push(vcpkg);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_meson(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
let mut written = vec![];
fs::create_dir_all(out)?;
let build = out.join("meson.build");
if !build.exists() || force {
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let license = pkg.get("license").cloned().unwrap_or_else(|| "MIT".to_string());
let mut s = format!("project('{name}', 'c', 'cpp',\n version: '{version}',\n license: '{license}',\n");
s.push_str(" default_options: ['cpp_std=c++20'])\n\n");
s.push_str(&format!("executable('{name}', 'src/main.cpp')\n"));
fs::write(&build, s)?;
written.push(build);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_fpm(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::toml_ast::{Document, Value as TVal};
let mut written = vec![];
fs::create_dir_all(out)?;
let fpm = out.join("fpm.toml");
if !fpm.exists() || force {
let pkg = &c.package;
let mut doc = Document::new();
if let Some(n) = pkg.get("name") { doc.root_key("name", TVal::s(n)); }
if let Some(v) = pkg.get("version") { doc.root_key("version", TVal::s(v)); }
if let Some(l) = pkg.get("license") { doc.root_key("license", TVal::s(l)); }
let build = doc.table("build");
build.key("auto-executables", TVal::b(true));
build.key("auto-tests", TVal::b(true));
if let Some(d) = pkg.get("description") {
build.key("description", TVal::s(d));
}
fs::write(&fpm, doc.render())?;
written.push(fpm);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_gleam(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::toml_ast::{Document, Value as TVal};
let mut written = vec![];
fs::create_dir_all(out)?;
let g = out.join("gleam.toml");
if !g.exists() || force {
let pkg = &c.package;
let mut doc = Document::new();
if let Some(n) = pkg.get("name") { doc.root_key("name", TVal::s(n)); }
if let Some(v) = pkg.get("version") { doc.root_key("version", TVal::s(v)); }
if let Some(l) = pkg.get("license") {
doc.root_key("licences", TVal::arr([TVal::s(l)]));
}
if let Some(d) = pkg.get("description") {
doc.root_key("description", TVal::s(d));
}
doc.table("dependencies").key("gleam_stdlib", TVal::s(">= 0.34.0 and < 2.0.0"));
doc.table("dev-dependencies").key("gleeunit", TVal::s(">= 1.0.0 and < 2.0.0"));
fs::write(&g, doc.render())?;
written.push(g);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_alire(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::toml_ast::{Document, Value as TVal};
let mut written = vec![];
fs::create_dir_all(out)?;
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let a = out.join(format!("alire-{name}.toml"));
if !a.exists() || force {
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let mut doc = Document::new();
doc.root_key("name", TVal::s(&name));
doc.root_key("version", TVal::s(version));
if let Some(d) = pkg.get("description") { doc.root_key("description", TVal::s(d)); }
if let Some(l) = pkg.get("license") { doc.root_key("licenses", TVal::s(l)); }
doc.root_key("authors", TVal::arr([TVal::s("pleme-io")]));
doc.root_key("maintainers", TVal::arr([TVal::s("engineering@pleme.io")]));
doc.root_key("maintainers-logins", TVal::arr([TVal::s("pleme-io")]));
doc.array_table("depends-on").key("gnat", TVal::s(">=11"));
fs::write(&a, doc.render())?;
written.push(a);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_cabal(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::control_ast::{render as render_ctrl, Line};
let mut written = vec![];
fs::create_dir_all(out)?;
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let cab = out.join(format!("{name}.cabal"));
if !cab.exists() || force {
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0.0".to_string());
let mut lines: Vec<Line> = vec![
Line::Field { key: "cabal-version".into(), value: "2.4".into() },
Line::Field { key: "name".into(), value: name },
Line::Field { key: "version".into(), value: version },
];
if let Some(d) = pkg.get("description") {
lines.push(Line::Field { key: "synopsis".into(), value: d.clone() });
lines.push(Line::Field { key: "description".into(), value: d.clone() });
}
if let Some(l) = pkg.get("license") {
lines.push(Line::Field { key: "license".into(), value: l.clone() });
}
lines.push(Line::Field { key: "author".into(), value: "pleme-io".into() });
lines.push(Line::Field { key: "maintainer".into(), value: "engineering@pleme.io".into() });
lines.push(Line::Field { key: "build-type".into(), value: "Simple".into() });
lines.push(Line::Blank);
lines.push(Line::Stanza {
header: "library".into(),
body: vec![
Line::Field { key: "exposed-modules".into(), value: "Lib".into() },
Line::Field { key: "hs-source-dirs".into(), value: "src".into() },
Line::Field { key: "build-depends".into(), value: "base >= 4.14 && < 5".into() },
Line::Field { key: "default-language".into(), value: "Haskell2010".into() },
],
});
crate::ast::emit(&cab, &crate::control_ast::Document::from(2, lines))?;
written.push(cab);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_racket(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::sexp_ast::{render_forms, SExp};
let mut written = vec![];
fs::create_dir_all(out)?;
let info = out.join("info.rkt");
if !info.exists() || force {
let pkg = &c.package;
let name = pkg.get("name").cloned().unwrap_or_else(|| c.name.clone());
let version = pkg.get("version").cloned().unwrap_or_else(|| "0.1.0".to_string());
let mut forms: Vec<SExp> = vec![
SExp::Symbol("#lang info".to_string()),
SExp::list([SExp::sym("define"), SExp::sym("collection"), SExp::str(name)]),
SExp::list([SExp::sym("define"), SExp::sym("version"), SExp::str(version)]),
SExp::list([
SExp::sym("define"),
SExp::sym("deps"),
SExp::Symbol("'(\"base\")".to_string()),
]),
];
if let Some(d) = pkg.get("description") {
forms.push(SExp::list([SExp::sym("define"), SExp::sym("pkg-desc"), SExp::str(d)]));
}
forms.push(SExp::list([
SExp::sym("define"),
SExp::sym("pkg-authors"),
SExp::Symbol("'(pleme-io)".to_string()),
]));
fs::write(&info, render_forms(&forms))?;
written.push(info);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_julia(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::toml_ast::{Document, Value};
let mut written = vec![];
fs::create_dir_all(out)?;
let project = out.join("Project.toml");
if !project.exists() || force {
let pkg = &c.package;
let mut doc = Document::new();
if let Some(n) = pkg.get("name") { doc.root_key("name", Value::s(n)); }
let uuid = pkg.get("uuid").cloned()
.unwrap_or_else(|| "00000000-0000-0000-0000-000000000000".to_string());
doc.root_key("uuid", Value::s(uuid));
if let Some(v) = pkg.get("version") { doc.root_key("version", Value::s(v)); }
if let Some(authors) = pkg.get("authors") {
doc.root_key("authors", Value::arr([Value::s(authors)]));
}
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
if !deps.runtime.is_empty() {
let deps_tbl = doc.table("deps");
for d in &deps.runtime {
if let (Some(n), Some(v)) = (d.get("name"), d.get("uuid")) {
deps_tbl.key(n, Value::s(v));
}
}
let compat = doc.table("compat");
if let Some(supports) = c.supports_raw.as_deref() {
if let Some(jv) = read_keyword_in_block(supports, ":julia") {
compat.key("julia", Value::s(jv.trim_matches('"')));
}
}
}
fs::write(&project, doc.render())?;
written.push(project);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
#[derive(Debug, Default)]
pub struct Caixa {
pub name: String,
pub kind: String,
pub ecosystem: String,
pub package: BTreeMap<String, String>,
pub package_lists: BTreeMap<String, Vec<String>>,
pub workflows: Vec<String>,
pub stacks: Vec<String>,
pub depends_on: Vec<String>,
pub exposes: Vec<String>,
pub publish_to_git: bool,
pub ci_config: BTreeMap<String, BTreeMap<String, String>>,
pub dependencies_raw: Option<String>,
pub features_raw: Option<String>,
pub profiles_raw: Option<String>,
pub scripts_raw: Option<String>,
pub sources_raw: Option<String>,
pub supports_raw: Option<String>,
pub artifacts_raw: Option<String>,
pub publish_raw: Option<String>,
pub nix_modules: bool,
pub deps_transitive: bool,
pub deps_sync_target: String,
}
fn parse(src: &str) -> Result<Caixa> {
let cleaned: String = src
.lines()
.map(|l| match l.find(";;") {
Some(i) => &l[..i],
None => l,
})
.collect::<Vec<_>>()
.join("\n");
let cleaned = cleaned.trim();
if !cleaned.starts_with("(defcaixa") {
bail!("source does not start with (defcaixa ...)");
}
let after_keyword = &cleaned[("(defcaixa".len())..].trim_start();
let name_end = after_keyword
.find(|c: char| c.is_whitespace())
.unwrap_or(after_keyword.len());
let name = after_keyword[..name_end].to_string();
let body = &after_keyword[name_end..];
let mut out = Caixa {
name,
nix_modules: true,
deps_transitive: true,
deps_sync_target: "both".to_string(),
..Default::default()
};
out.kind = read_keyword(body, ":kind").unwrap_or_default();
out.ecosystem = read_keyword(body, ":ecosystem").unwrap_or_default();
if let Some(pkg_block) = read_dict_block(body, ":package") {
out.package = parse_dict(&pkg_block);
if let Some(cats) = read_vector_in_dict(&pkg_block, ":categories") {
out.package_lists.insert("categories".into(), cats);
}
if let Some(kws) = read_vector_in_dict(&pkg_block, ":keywords") {
out.package_lists.insert("keywords".into(), kws);
}
}
if let Some(wf) = read_vector(body, ":workflows") {
out.workflows = wf;
}
if let Some(stacks) = read_vector(body, ":stacks") {
out.stacks = stacks;
}
if let Some(deps) = read_vector(body, ":depends-on") {
out.depends_on = deps;
}
if let Some(exposes) = read_vector(body, ":exposes") {
out.exposes = exposes;
}
if let Some(pub_to_git) = read_keyword(body, ":publish-to-git") {
out.publish_to_git = pub_to_git == "true";
}
if let Some(nm) = read_keyword(body, ":nix-modules") {
out.nix_modules = nm != "false";
}
if let Some(t) = read_keyword(body, ":deps-transitive") {
out.deps_transitive = t != "false";
}
if let Some(s) = read_keyword(body, ":deps-sync-target") {
out.deps_sync_target = s;
}
out.dependencies_raw = read_dict_block(body, ":dependencies");
out.features_raw = read_dict_block(body, ":features");
out.profiles_raw = read_dict_block(body, ":profiles");
out.scripts_raw = read_dict_block(body, ":scripts");
out.sources_raw = read_dict_block(body, ":sources");
out.supports_raw = read_dict_block(body, ":supports");
out.artifacts_raw = read_dict_block(body, ":artifacts");
out.publish_raw = read_dict_block(body, ":publish");
if let Some(ci_block) = read_dict_block(body, ":ci-config") {
for sub_key in ["bump", "publish", "security", "validation"] {
if let Some(sub_block) = read_dict_block(&ci_block, &format!(":{sub_key}")) {
out.ci_config.insert(sub_key.into(), parse_dict(&sub_block));
}
}
}
Ok(out)
}
fn find_after_keyword<'a>(s: &'a str, kw: &str) -> Option<&'a str> {
let mut start = 0;
while let Some(i) = s[start..].find(kw) {
let abs = start + i;
let after_kw = &s[abs + kw.len()..];
if after_kw.starts_with(|c: char| c.is_whitespace()) {
return Some(after_kw.trim_start());
}
start = abs + kw.len();
}
None
}
fn read_keyword(s: &str, kw: &str) -> Option<String> {
let after = find_after_keyword(s, kw)?;
let end = after
.find(|c: char| c.is_whitespace() || c == ')' || c == '}')
.unwrap_or(after.len());
Some(after[..end].trim_matches(':').to_string())
}
fn read_dict_block(s: &str, kw: &str) -> Option<String> {
let after = find_after_keyword(s, kw)?;
let after = after.trim_start();
if !after.starts_with('{') {
return None;
}
let mut depth = 0;
let mut end = 0;
for (j, c) in after.char_indices() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
end = j + 1;
break;
}
}
_ => {}
}
}
if end == 0 {
return None;
}
Some(after[1..end - 1].to_string())
}
fn read_vector(s: &str, kw: &str) -> Option<Vec<String>> {
let after = find_after_keyword(s, kw)?;
if !after.starts_with('[') {
return None;
}
let mut depth = 0;
let mut end = 0;
for (j, c) in after.char_indices() {
match c {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
end = j + 1;
break;
}
}
_ => {}
}
}
if end == 0 {
return None;
}
let inner = &after[1..end - 1];
Some(
inner
.split_whitespace()
.map(|w| w.trim_matches('"').trim_start_matches(':').to_string())
.filter(|s| !s.is_empty())
.collect(),
)
}
fn read_vector_in_dict(dict: &str, kw: &str) -> Option<Vec<String>> {
read_vector(dict, kw)
}
fn parse_dict(inner: &str) -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
let mut chars = inner.char_indices().peekable();
while let Some((i, c)) = chars.next() {
if c == ':' {
let mut end = i + 1;
while end < inner.len() && !inner.as_bytes()[end].is_ascii_whitespace() {
end += 1;
}
let kw = inner[i + 1..end].to_string();
let mut val_start = end;
while val_start < inner.len() && inner.as_bytes()[val_start].is_ascii_whitespace() {
val_start += 1;
}
if val_start >= inner.len() {
break;
}
if inner.as_bytes()[val_start] == b'{' || inner.as_bytes()[val_start] == b'[' {
let mut depth = 0;
let open = inner.as_bytes()[val_start] as char;
let close = if open == '{' { '}' } else { ']' };
let mut p = val_start;
for (j, c2) in inner[val_start..].char_indices() {
match c2 {
c if c == open => depth += 1,
c if c == close => {
depth -= 1;
if depth == 0 {
p = val_start + j + 1;
break;
}
}
_ => {}
}
}
while let Some(&(k, _)) = chars.peek() {
if k >= p {
break;
}
chars.next();
}
continue;
}
if inner.as_bytes()[val_start] == b'"' {
let mut end = val_start + 1;
while end < inner.len() && inner.as_bytes()[end] != b'"' {
end += 1;
}
out.insert(kw, inner[val_start + 1..end].to_string());
while let Some(&(k, _)) = chars.peek() {
if k > end {
break;
}
chars.next();
}
} else {
let mut end = val_start;
while end < inner.len() && !inner.as_bytes()[end].is_ascii_whitespace() {
end += 1;
}
out.insert(kw, inner[val_start..end].trim_start_matches(':').to_string());
while let Some(&(k, _)) = chars.peek() {
if k >= end {
break;
}
chars.next();
}
}
}
}
out
}
fn render_rust_single(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::toml_ast::{Document, Value as TVal};
let mut written = vec![];
fs::create_dir_all(out)?;
let wf_dir = out.join(".github/workflows");
fs::create_dir_all(&wf_dir)?;
let cargo_path = out.join("Cargo.toml");
if !cargo_path.exists() || force {
let pkg = &c.package;
let mut doc = Document::new();
{
let p = doc.table("package");
if let Some(n) = pkg.get("name") { p.key("name", TVal::s(n)); }
if let Some(v) = pkg.get("version") { p.key("version", TVal::s(v)); }
p.key("edition", TVal::s("2024"));
if let Some(supports) = c.supports_raw.as_deref() {
if let Some(rust_req) = read_keyword_in_block(supports, ":rust") {
let clean = rust_req.trim_matches('"').trim_start_matches(">=").to_string();
p.key("rust-version", TVal::s(clean));
}
}
if let Some(d) = pkg.get("description") { p.key("description", TVal::s(d)); }
if let Some(l) = pkg.get("license") { p.key("license", TVal::s(l)); }
if let Some(r) = pkg.get("repository") { p.key("repository", TVal::s(r)); }
if let Some(h) = pkg.get("homepage") { p.key("homepage", TVal::s(h)); }
p.key("readme", TVal::s("README.md"));
let authors = pkg.get("authors")
.cloned()
.unwrap_or_else(|| "pleme-io".to_string());
p.key("authors", TVal::arr([TVal::s(authors)]));
if let Some(cats) = c.package_lists.get("categories") {
p.key("categories", TVal::arr(cats.iter().map(TVal::s)));
}
if let Some(kws) = c.package_lists.get("keywords") {
p.key("keywords", TVal::arr(kws.iter().map(TVal::s)));
}
}
let typed_cargo = doc.render();
let mut cargo = typed_cargo;
if let Some(artifacts) = c.artifacts_raw.as_deref() {
for bin in extract_bin_entries(artifacts) {
cargo.push_str("\n[[bin]]\n");
if let Some(n) = bin.get("name") {
let _ = writeln!(cargo, "name = {}", TVal::s(n).render_inline());
}
if let Some(p) = bin.get("path") {
let _ = writeln!(cargo, "path = {}", TVal::s(p).render_inline());
}
}
}
if let Some(publish) = c.publish_raw.as_deref() {
let publish_fields = emit_publish_fields(publish);
if !publish_fields.is_empty() {
cargo.push_str(&publish_fields);
}
}
{
let mut lints_doc = Document::new();
lints_doc.table("lints.clippy").key("pedantic", TVal::s("warn"));
cargo.push('\n');
cargo.push_str(&lints_doc.render());
}
if let Some(profiles) = c.profiles_raw.as_deref() {
cargo.push_str(&emit_profiles_blocks(profiles));
}
if let Some(features) = c.features_raw.as_deref() {
let features_block = emit_features_block(features);
if !features_block.is_empty() {
cargo.push_str("\n[features]\n");
cargo.push_str(&features_block);
}
}
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
cargo.push_str("\n[dependencies]\n");
for d in &deps.runtime {
cargo.push_str(&emit_cargo_dep(d));
}
if !deps.dev.is_empty() {
cargo.push_str("\n[dev-dependencies]\n");
for d in &deps.dev {
cargo.push_str(&emit_cargo_dep(d));
}
}
if !deps.build.is_empty() {
cargo.push_str("\n[build-dependencies]\n");
for d in &deps.build {
cargo.push_str(&emit_cargo_dep(d));
}
}
fs::write(&cargo_path, cargo)?;
written.push(cargo_path);
}
let cfg_path = out.join(".pleme-io-release.toml");
if !cfg_path.exists() || force {
let mut cfg = String::from("# Generated by pleme-doc-gen caixa\n");
for (section, fields) in &c.ci_config {
cfg.push_str(&format!("[{section}]\n"));
for (k, v) in fields {
let v = if matches!(v.as_str(), "true" | "false") {
v.clone()
} else {
format!("\"{v}\"")
};
cfg.push_str(&format!("{k} = {v}\n"));
}
cfg.push('\n');
}
fs::write(&cfg_path, cfg)?;
written.push(cfg_path);
}
for wf in &c.workflows {
let path = wf_dir.join(format!("{wf}.yml"));
if path.exists() && !force {
continue;
}
let content = match wf.as_str() {
"auto-release" => AUTO_RELEASE_SHIM,
"pre-merge-gate" => PRE_MERGE_GATE_SHIM,
"security-gate" => SECURITY_GATE_SHIM,
_ => continue,
};
fs::write(&path, content)?;
written.push(path);
}
emit_stacks_workflow(c, &wf_dir, force, &mut written)?;
if c.nix_modules {
emit_nix_module_trio(c, out, force, &mut written)?;
}
emit_deps_sync(c, out, force, &mut written)?;
emit_flake_nix(c, out, force, &mut written)?;
Ok(written)
}
fn render_ci_shims(c: &Caixa, out: &Path, force: bool, written: &mut Vec<PathBuf>) -> Result<()> {
let wf_dir = out.join(".github/workflows");
fs::create_dir_all(&wf_dir)?;
let cfg_path = out.join(".pleme-io-release.toml");
if !cfg_path.exists() || force {
let mut cfg = String::from("# Generated by pleme-doc-gen caixa\n");
for (section, fields) in &c.ci_config {
cfg.push_str(&format!("[{section}]\n"));
for (k, v) in fields {
let v = if matches!(v.as_str(), "true" | "false") {
v.clone()
} else {
format!("\"{v}\"")
};
cfg.push_str(&format!("{k} = {v}\n"));
}
cfg.push('\n');
}
fs::write(&cfg_path, cfg)?;
written.push(cfg_path);
}
for wf in &c.workflows {
let path = wf_dir.join(format!("{wf}.yml"));
if path.exists() && !force {
continue;
}
let content = match wf.as_str() {
"auto-release" => AUTO_RELEASE_SHIM,
"pre-merge-gate" => PRE_MERGE_GATE_SHIM,
"security-gate" => SECURITY_GATE_SHIM,
_ => continue,
};
fs::write(&path, content)?;
written.push(path);
}
emit_stacks_workflow(c, &wf_dir, force, written)?;
let out_dir = wf_dir.parent().unwrap().parent().unwrap().to_path_buf();
if c.nix_modules {
emit_nix_module_trio(c, &out_dir, force, written)?;
}
emit_deps_sync(c, &out_dir, force, written)?;
emit_flake_nix(c, &out_dir, force, written)?;
Ok(())
}
fn read_keyword_in_block(block: &str, kw: &str) -> Option<String> {
read_keyword(block, kw)
}
#[derive(Debug, Default)]
struct CaixaDeps {
runtime: Vec<BTreeMap<String, String>>,
dev: Vec<BTreeMap<String, String>>,
build: Vec<BTreeMap<String, String>>,
}
fn parse_deps_raw(raw: &str) -> CaixaDeps {
let mut out = CaixaDeps::default();
for kw in [":runtime", ":dev", ":build"] {
if let Some(vec_src) = extract_vector_block(raw, kw) {
let class_deps = parse_dep_vector(&vec_src);
match kw {
":runtime" => out.runtime = class_deps,
":dev" => out.dev = class_deps,
":build" => out.build = class_deps,
_ => {}
}
}
}
out
}
fn extract_vector_block(s: &str, kw: &str) -> Option<String> {
let after = find_after_keyword(s, kw)?;
if !after.starts_with('[') {
return None;
}
let mut depth = 0;
let mut end = 0;
for (j, c) in after.char_indices() {
match c {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
end = j + 1;
break;
}
}
_ => {}
}
}
if end == 0 {
return None;
}
Some(after[1..end - 1].to_string())
}
fn parse_dep_vector(s: &str) -> Vec<BTreeMap<String, String>> {
let mut out = vec![];
let mut depth = 0;
let mut start = None;
for (i, c) in s.char_indices() {
match c {
'{' => {
if depth == 0 {
start = Some(i + 1);
}
depth += 1;
}
'}' => {
depth -= 1;
if depth == 0 {
if let Some(s_start) = start {
out.push(parse_dict(&s[s_start..i]));
start = None;
}
}
}
_ => {}
}
}
out
}
fn emit_cargo_dep(d: &BTreeMap<String, String>) -> String {
let name = d.get("name").cloned().unwrap_or_default();
let version = d.get("version").cloned().unwrap_or_else(|| "*".to_string());
if name.is_empty() {
return String::new();
}
if let Some(feats_raw) = d.get("features") {
let feats: Vec<String> = feats_raw
.trim_start_matches('[')
.trim_end_matches(']')
.split_whitespace()
.map(|s| s.trim_matches('"').trim_start_matches(':').to_string())
.filter(|s| !s.is_empty())
.collect();
if !feats.is_empty() {
let joined: Vec<String> = feats.iter().map(|f| format!("\"{f}\"")).collect();
return format!("{name} = {{ version = \"{version}\", features = [{}] }}\n", joined.join(", "));
}
}
format!("{name} = \"{version}\"\n")
}
fn emit_features_block(features_raw: &str) -> String {
let mut out = String::new();
let mut chars = features_raw.char_indices().peekable();
let mut current_key: Option<String> = None;
while let Some((i, c)) = chars.next() {
if c == ':' {
let mut end = i + 1;
while end < features_raw.len() && !features_raw.as_bytes()[end].is_ascii_whitespace() {
end += 1;
}
current_key = Some(features_raw[i + 1..end].to_string());
while let Some(&(k, ch)) = chars.peek() {
if k <= end || ch.is_whitespace() {
chars.next();
} else {
break;
}
}
if let Some(&(_, ch)) = chars.peek() {
if ch == '[' {
let rest = &features_raw[end..];
if let Some(open) = rest.find('[') {
let mut depth = 0;
let mut close = 0;
for (j, c) in rest[open..].char_indices() {
match c {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
close = open + j + 1;
break;
}
}
_ => {}
}
}
if close > 0 {
let inner = &rest[open + 1..close - 1];
let items: Vec<String> = inner
.split_whitespace()
.map(|s| format!("\"{}\"", s.trim_matches('"').trim_start_matches(':')))
.filter(|s| s != "\"\"")
.collect();
if let Some(key) = ¤t_key {
out.push_str(&format!("{key} = [{}]\n", items.join(", ")));
}
while let Some(&(k, _)) = chars.peek() {
if k >= end + close {
break;
}
chars.next();
}
}
}
} else if ch == '{' {
let after = &features_raw[end..];
if let Some(open) = after.find('{') {
let mut depth = 0;
let mut close = 0;
for (j, c) in after[open..].char_indices() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
close = open + j + 1;
break;
}
}
_ => {}
}
}
if close > 0 {
let dict_inner = &after[open + 1..close - 1];
if let Some(adds_vec) = extract_vector_block(dict_inner, ":adds") {
let items: Vec<String> = adds_vec
.split_whitespace()
.map(|s| format!("\"{}\"", s.trim_matches('"').trim_start_matches(':')))
.filter(|s| s != "\"\"")
.collect();
if let Some(key) = ¤t_key {
out.push_str(&format!("{key} = [{}]\n", items.join(", ")));
}
}
let abs_close = end + close;
while let Some(&(k, _)) = chars.peek() {
if k >= abs_close {
break;
}
chars.next();
}
}
}
}
}
}
}
out
}
fn emit_profiles_blocks(profiles_raw: &str) -> String {
let mut out = String::new();
for profile_name in ["release", "dev", "bench", "test"] {
let kw = format!(":{profile_name}");
if let Some(block) = read_dict_block(profiles_raw, &kw) {
out.push_str(&format!("\n[profile.{profile_name}]\n"));
let fields = parse_dict(&block);
for (k, v) in &fields {
let val = if matches!(v.as_str(), "true" | "false")
|| v.parse::<i64>().is_ok()
{
v.clone()
} else {
format!("\"{v}\"")
};
out.push_str(&format!("{k} = {val}\n"));
}
}
}
out
}
fn emit_publish_fields(publish_raw: &str) -> String {
let mut out = String::new();
let fields = parse_dict(publish_raw);
if let Some(v) = fields.get("private") {
if v == "true" {
out.push_str("publish = false\n");
}
}
if let Some(incl) = extract_vector_block(publish_raw, ":files-include") {
let items: Vec<String> = incl
.split_whitespace()
.map(|s| format!("\"{}\"", s.trim_matches('"')))
.filter(|s| s != "\"\"")
.collect();
if !items.is_empty() {
out.push_str(&format!("include = [{}]\n", items.join(", ")));
}
}
if let Some(excl) = extract_vector_block(publish_raw, ":files-exclude") {
let items: Vec<String> = excl
.split_whitespace()
.map(|s| format!("\"{}\"", s.trim_matches('"')))
.filter(|s| s != "\"\"")
.collect();
if !items.is_empty() {
out.push_str(&format!("exclude = [{}]\n", items.join(", ")));
}
}
out
}
fn extract_bin_entries(artifacts: &str) -> Vec<BTreeMap<String, String>> {
extract_vector_block(artifacts, ":bin")
.map(|v| parse_dep_vector(&v))
.unwrap_or_default()
}
fn emit_nix_module_trio(
c: &Caixa,
out: &Path,
force: bool,
written: &mut Vec<PathBuf>,
) -> Result<()> {
let mod_dir = out.join("nix/modules");
fs::create_dir_all(&mod_dir)?;
let name = &c.name;
let desc = c.package.get("description").cloned().unwrap_or_default();
let nixos = format!(
"# nix/modules/nixos.nix — auto-generated from {name}.caixa.lisp
# {desc}
{{ config, lib, pkgs, ... }}:
let
cfg = config.services.{name};
in {{
options.services.{name} = {{
enable = lib.mkEnableOption \"{name}\";
package = lib.mkOption {{
type = lib.types.package;
default = pkgs.{name} or null;
}};
}};
config = lib.mkIf cfg.enable {{
environment.systemPackages = [ cfg.package ];
}};
}}
"
);
let darwin = format!(
"# nix/modules/darwin.nix — auto-generated from {name}.caixa.lisp
{{ config, lib, pkgs, ... }}:
let cfg = config.services.{name}; in {{
options.services.{name} = {{
enable = lib.mkEnableOption \"{name}\";
package = lib.mkOption {{ type = lib.types.package; default = pkgs.{name} or null; }};
}};
config = lib.mkIf cfg.enable {{
environment.systemPackages = [ cfg.package ];
}};
}}
"
);
let hm = format!(
"# nix/modules/home-manager.nix — auto-generated from {name}.caixa.lisp
{{ config, lib, pkgs, ... }}:
let cfg = config.programs.{name}; in {{
options.programs.{name} = {{
enable = lib.mkEnableOption \"{name}\";
package = lib.mkOption {{ type = lib.types.package; default = pkgs.{name} or null; }};
}};
config = lib.mkIf cfg.enable {{ home.packages = [ cfg.package ]; }};
}}
"
);
for (file, content) in [("nixos.nix", nixos), ("darwin.nix", darwin), ("home-manager.nix", hm)] {
let p = mod_dir.join(file);
if !p.exists() || force {
fs::write(&p, content)?;
written.push(p);
}
}
Ok(())
}
fn emit_deps_sync(
c: &Caixa,
out: &Path,
force: bool,
written: &mut Vec<PathBuf>,
) -> Result<()> {
if c.depends_on.is_empty() {
return Ok(());
}
let sync = c.deps_sync_target.as_str();
if matches!(sync, "nix" | "both") {
let nix_fragment = out.join("nix/caixa-deps.nix");
fs::create_dir_all(out.join("nix"))?;
if !nix_fragment.exists() || force {
let mut s = String::from("# Auto-generated from caixa :depends-on.\n");
s.push_str("# Splice into flake.nix `inputs = { ... };` block.\n{\n");
for dep in &c.depends_on {
let parts: Vec<&str> = dep.splitn(2, '@').collect();
let repo = parts[0];
let rev = parts.get(1).copied().unwrap_or("main");
let key = repo.rsplit('/').next().unwrap_or("dep").replace('-', "_");
s.push_str(&format!(
" {key} = {{ url = \"github:{repo}/{rev}\"; flake = true; }};\n"
));
}
s.push_str("}\n");
fs::write(&nix_fragment, s)?;
written.push(nix_fragment);
}
}
if matches!(sync, "local" | "both") {
let manifest = out.join(".caixa-deps").join("MANIFEST.txt");
fs::create_dir_all(out.join(".caixa-deps"))?;
if !manifest.exists() || force {
let mut s = String::from("# Auto-generated from caixa :depends-on.\n");
for dep in &c.depends_on {
s.push_str(&format!("{dep}\n"));
}
if c.deps_transitive {
s.push_str("\n# :deps-transitive=true — caixa-deps-resolve recurses.\n");
}
fs::write(&manifest, s)?;
written.push(manifest);
}
}
Ok(())
}
fn emit_flake_nix(
c: &Caixa,
out: &Path,
force: bool,
written: &mut Vec<PathBuf>,
) -> Result<()> {
let path = out.join("flake.nix");
if path.exists() && !force {
return Ok(());
}
let name = &c.name;
let mut inputs = String::from(
" nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n \
substrate = {\n url = \"github:pleme-io/substrate\";\n \
inputs.nixpkgs.follows = \"nixpkgs\";\n };\n \
flake-parts.url = \"github:hercules-ci/flake-parts\";\n",
);
for dep in &c.depends_on {
let parts: Vec<&str> = dep.splitn(2, '@').collect();
let repo = parts[0];
let rev = parts.get(1).copied().unwrap_or("main");
let key = repo.rsplit('/').next().unwrap_or("dep").replace('-', "_");
inputs.push_str(&format!(
" {key} = {{\n url = \"github:{repo}/{rev}\";\n \
inputs.nixpkgs.follows = \"nixpkgs\";\n }};\n"
));
}
let builder = match c.ecosystem.as_str() {
"rust-single-crate" => "substrate.lib.${system}.rustToolReleaseFlakeBuilder",
"rust-workspace" => "substrate.lib.${system}.rustWorkspaceReleaseFlakeBuilder",
"npm" => "substrate.lib.${system}.typescriptToolFlakeBuilder",
"python" => "substrate.lib.${system}.pythonToolFlakeBuilder",
"helm" => "substrate.lib.${system}.helmChartFlakeBuilder",
"github-action" => "substrate.lib.${system}.rustToolReleaseFlakeBuilder",
_ => "substrate.lib.${system}.rustToolReleaseFlakeBuilder",
};
let content = format!(
"# flake.nix — auto-generated from {name}.caixa.lisp
# Edit caixa source + re-render via:
# pleme-doc-gen caixa --source {name}.caixa.lisp --out . --force
{{
description = \"{name} — caixa-rendered Nix flake\";
inputs = {{
{inputs} }};
outputs = inputs @ {{ self, nixpkgs, substrate, flake-parts, ... }}:
flake-parts.lib.mkFlake {{ inherit inputs; }} {{
systems = [ \"aarch64-darwin\" \"x86_64-linux\" \"aarch64-linux\" ];
perSystem = {{ pkgs, system, ... }}: let
builder = {builder};
in {{
packages.default = builder {{
inherit pkgs;
name = \"{name}\";
src = ./.;
}};
devShells.default = pkgs.mkShell {{
buildInputs = [ pkgs.git ];
}};
}};
}};
}}
"
);
fs::write(&path, content)?;
written.push(path);
Ok(())
}
fn emit_stacks_workflow(
c: &Caixa,
wf_dir: &Path,
force: bool,
written: &mut Vec<PathBuf>,
) -> Result<()> {
if c.stacks.is_empty() {
return Ok(());
}
let stacks_path = wf_dir.join("pleme-stacks.yml");
if stacks_path.exists() && !force {
return Ok(());
}
let mut content = String::from(
"name: pleme-stacks\n# Auto-generated from caixa :stacks slot.\n\n",
);
content.push_str("on:\n push: { branches: [main] }\n pull_request: { branches: [main] }\n\n");
content.push_str("permissions:\n contents: read\n packages: write\n id-token: write\n\n");
content.push_str("jobs:\n");
for stack in &c.stacks {
let job_name = stack.replace('-', "_");
content.push_str(&format!(" {job_name}:\n"));
content.push_str(&format!(
" uses: pleme-io/substrate/.github/workflows/{stack}.yml@main\n"
));
content.push_str(" secrets: inherit\n");
}
fs::write(&stacks_path, content)?;
written.push(stacks_path.to_path_buf());
Ok(())
}
fn render_npm(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::json_ast::{render as render_json, Value};
let mut written = vec![];
fs::create_dir_all(out)?;
let pkg_json = out.join("package.json");
if !pkg_json.exists() || force {
let pkg = &c.package;
let mut json = Value::obj();
if let Some(n) = pkg.get("name") { json.insert("name", Value::s(n)); }
if let Some(v) = pkg.get("version") { json.insert("version", Value::s(v)); }
if let Some(d) = pkg.get("description") { json.insert("description", Value::s(d)); }
if let Some(l) = pkg.get("license") { json.insert("license", Value::s(l)); }
if let Some(r) = pkg.get("repository") {
let mut repo_obj = Value::obj();
repo_obj.insert("type", Value::s("git"));
repo_obj.insert("url", Value::s(r));
json.insert("repository", repo_obj);
}
if let Some(kws) = c.package_lists.get("keywords") {
json.insert("keywords", Value::arr(kws.iter().map(Value::s)));
}
if let Some(supports) = c.supports_raw.as_deref() {
if let Some(node_req) = read_keyword_in_block(supports, ":node") {
let mut engines = Value::obj();
engines.insert("node", Value::s(node_req.trim_matches('"')));
json.insert("engines", engines);
}
}
if let Some(publish) = c.publish_raw.as_deref() {
let fields = parse_dict(publish);
if fields.get("private").map(String::as_str) == Some("true") {
json.insert("private", Value::b(true));
}
if let Some(files_vec) = extract_vector_block(publish, ":files-include") {
let items: Vec<Value> = files_vec
.split_whitespace()
.map(|s| Value::s(s.trim_matches('"')))
.filter(|v| matches!(v, Value::String(s) if !s.is_empty()))
.collect();
if !items.is_empty() {
json.insert("files", Value::Array(items));
}
}
}
json.insert("main", Value::s("index.js"));
let mut scripts_obj = Value::obj();
if let Some(scripts) = c.scripts_raw.as_deref() {
let s_fields = parse_dict(scripts);
for (k, v) in &s_fields {
scripts_obj.insert(k.as_str(), Value::s(v));
}
}
if matches!(&scripts_obj, Value::Object(items) if items.is_empty()) {
scripts_obj.insert("test", Value::s("echo OK"));
}
json.insert("scripts", scripts_obj);
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
emit_npm_deps_block_ast(&mut json, "dependencies", &deps.runtime);
emit_npm_deps_block_ast(&mut json, "devDependencies", &deps.dev);
crate::ast::emit(&pkg_json, &json)?;
written.push(pkg_json);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn emit_npm_deps_block_ast(
json: &mut crate::json_ast::Value,
key: &str,
deps: &[BTreeMap<String, String>],
) {
use crate::json_ast::Value;
if deps.is_empty() { return; }
let mut block = Value::obj();
for d in deps {
if let Some(name) = d.get("name") {
let ver = d.get("version").cloned().unwrap_or_else(|| "*".to_string());
block.insert(name.as_str(), Value::s(ver));
}
}
json.insert(key, block);
}
fn emit_npm_deps_block(json: &mut String, key: &str, deps: &[BTreeMap<String, String>]) {
if deps.is_empty() {
return;
}
let entries: Vec<String> = deps.iter()
.filter_map(|d| {
let name = d.get("name")?;
let ver = d.get("version").cloned().unwrap_or_else(|| "*".to_string());
Some(format!(" \"{name}\": \"{ver}\""))
})
.collect();
if !entries.is_empty() {
json.push_str(&format!(" \"{key}\": {{\n{}\n }},\n", entries.join(",\n")));
}
}
fn render_python(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::toml_ast::{Document, Value as TVal};
let mut written = vec![];
fs::create_dir_all(out)?;
let pyproj = out.join("pyproject.toml");
if !pyproj.exists() || force {
let pkg = &c.package;
let mut doc = Document::new();
{
let p = doc.table("project");
if let Some(n) = pkg.get("name") { p.key("name", TVal::s(n)); }
if let Some(v) = pkg.get("version") { p.key("version", TVal::s(v)); }
if let Some(d) = pkg.get("description") { p.key("description", TVal::s(d)); }
if let Some(l) = pkg.get("license") {
let mut inline = std::collections::BTreeMap::new();
inline.insert("text".to_string(), TVal::s(l));
p.key("license", TVal::InlineTable(inline));
}
if let Some(supports) = c.supports_raw.as_deref() {
if let Some(py_req) = read_keyword_in_block(supports, ":python") {
let clean = py_req.trim_matches('"').to_string();
p.key("requires-python", TVal::s(clean));
}
}
if c.supports_raw.is_none() {
if let Some(rp) = pkg.get("requires-python") {
p.key("requires-python", TVal::s(rp));
}
}
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
if !deps.runtime.is_empty() {
let items: Vec<TVal> = deps.runtime.iter()
.filter_map(|d| {
let n = d.get("name")?;
let v = d.get("version").cloned().unwrap_or_default();
Some(if v.is_empty() {
TVal::s(n.clone())
} else {
let mut s = n.clone();
s.push_str(">=");
s.push_str(&v);
TVal::s(s)
})
})
.collect();
p.key("dependencies", TVal::Array(items));
}
}
let deps = parse_deps_raw(c.dependencies_raw.as_deref().unwrap_or(""));
if !deps.dev.is_empty() {
let items: Vec<TVal> = deps.dev.iter()
.filter_map(|d| {
let n = d.get("name")?;
let v = d.get("version").cloned().unwrap_or_default();
Some(if v.is_empty() {
TVal::s(n.clone())
} else {
let mut s = n.clone();
s.push_str(">=");
s.push_str(&v);
TVal::s(s)
})
})
.collect();
doc.table("project.optional-dependencies").key("dev", TVal::Array(items));
}
if let Some(scripts) = c.scripts_raw.as_deref() {
let s_fields = parse_dict(scripts);
if !s_fields.is_empty() {
let scripts_t = doc.table("project.scripts");
for (k, v) in &s_fields {
scripts_t.key(k.clone(), TVal::s(v));
}
}
}
let bs = doc.table("build-system");
bs.key("requires", TVal::arr([TVal::s("hatchling")]));
bs.key("build-backend", TVal::s("hatchling.build"));
fs::write(&pyproj, doc.render())?;
written.push(pyproj);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_helm(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::yaml_ast::{render as render_yaml, Value};
let mut written = vec![];
fs::create_dir_all(out)?;
fs::create_dir_all(out.join("templates"))?;
let chart_yaml = out.join("Chart.yaml");
if !chart_yaml.exists() || force {
let pkg = &c.package;
let mut chart = Value::map();
chart.insert("apiVersion", Value::s("v2"));
if let Some(n) = pkg.get("name") { chart.insert("name", Value::s(n)); }
if let Some(d) = pkg.get("description") { chart.insert("description", Value::s(d)); }
chart.insert("type", Value::s(pkg.get("type").map(String::as_str).unwrap_or("application")));
if let Some(v) = pkg.get("version") { chart.insert("version", Value::s(v)); }
if let Some(av) = pkg.get("appVersion") { chart.insert("appVersion", Value::s(av)); }
crate::ast::emit(&chart_yaml, &chart)?;
written.push(chart_yaml);
}
let values = out.join("values.yaml");
if !values.exists() || force {
fs::write(&values, "# values.yaml — fill in per the chart's templates\n")?;
written.push(values);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_rust_workspace(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
use crate::toml_ast::{Document, Value as TVal};
let mut written = vec![];
fs::create_dir_all(out)?;
let cargo = out.join("Cargo.toml");
if !cargo.exists() || force {
let ws = &c.package;
let mut doc = Document::new();
{
let w = doc.table("workspace");
if let Some(members) = c.package_lists.get("members") {
w.key("members", TVal::arr(members.iter().map(TVal::s)));
}
w.key("resolver", TVal::s("2"));
}
{
let p = doc.table("workspace.package");
if let Some(v) = ws.get("version") { p.key("version", TVal::s(v)); }
p.key("edition", TVal::s("2024"));
if let Some(l) = ws.get("license") { p.key("license", TVal::s(l)); }
if let Some(r) = ws.get("repository") { p.key("repository", TVal::s(r)); }
}
fs::write(&cargo, doc.render())?;
written.push(cargo);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
fn render_github_action(c: &Caixa, out: &Path, force: bool) -> Result<Vec<PathBuf>> {
let mut written = vec![];
fs::create_dir_all(out)?;
let action_yml = out.join("action.yml");
if !action_yml.exists() || force {
let pkg = &c.package;
let mut yml = format!("name: 'pleme-io · {}'\n", c.name);
if let Some(d) = pkg.get("description") { yml.push_str(&format!("description: '{d}'\n")); }
yml.push_str("branding: { icon: 'box', color: 'green' }\n\n");
yml.push_str("inputs: {}\noutputs: {}\n\n");
yml.push_str("runs:\n using: composite\n steps:\n");
yml.push_str(" - id: src\n shell: bash\n run: |\n");
yml.push_str(" {\n echo 'script<<TLISP_EOF'\n");
yml.push_str(" curl -sL https://raw.githubusercontent.com/pleme-io/actions/main/_tlisp-stdlib/stdlib.tlisp\n");
yml.push_str(" echo\n cat ${{ github.action_path }}/run.tlisp\n");
yml.push_str(" echo 'TLISP_EOF'\n } >> \"$GITHUB_OUTPUT\"\n");
yml.push_str(" - id: run\n uses: pleme-io/actions/tatara-script@v1\n");
yml.push_str(" with:\n script: ${{ steps.src.outputs.script }}\n");
fs::write(&action_yml, yml)?;
written.push(action_yml);
}
let run_tlisp = out.join("run.tlisp");
if !run_tlisp.exists() || force {
fs::write(&run_tlisp, ";; run.tlisp — rendered from .caixa.lisp\n(log-info \"::warning::stub body\")\n(exit 0)\n")?;
written.push(run_tlisp);
}
render_ci_shims(c, out, force, &mut written)?;
Ok(written)
}
const AUTO_RELEASE_SHIM: &str = r#"name: auto-release
# Generated from caixa source. Edit caixa, re-render.
on:
push:
branches: [main]
workflow_dispatch:
inputs:
bump-type:
description: "patch | minor | major"
default: patch
jobs:
release:
uses: pleme-io/substrate/.github/workflows/auto-release.yml@main
with:
bump-type: ${{ inputs.bump-type || 'patch' }}
secrets: inherit
"#;
const PRE_MERGE_GATE_SHIM: &str = r#"name: pre-merge-gate
# Generated from caixa source. Edit caixa, re-render.
on:
pull_request:
branches: [main]
jobs:
gate:
uses: pleme-io/substrate/.github/workflows/pre-merge-gate.yml@main
secrets: inherit
"#;
const SECURITY_GATE_SHIM: &str = r#"name: security-gate
# Generated from caixa source. Edit caixa, re-render.
on:
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1'
jobs:
gate:
uses: pleme-io/substrate/.github/workflows/security-gate.yml@main
secrets: inherit
"#;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rust_library_caixa_renders() {
let src = r#"
(defcaixa my-lib
:kind :Biblioteca
:ecosystem :rust-single-crate
:package { :name "my-lib" :version "0.1.0" :description "A test lib." :license "MIT" }
:ci-config { :bump { :default-type "patch" :skip-when-no-source-changes true } }
:workflows [ :auto-release :pre-merge-gate :security-gate ])
"#;
let parsed = parse(src).unwrap();
assert_eq!(parsed.name, "my-lib");
assert_eq!(parsed.kind, "Biblioteca");
assert_eq!(parsed.ecosystem, "rust-single-crate");
assert_eq!(parsed.package.get("name").map(String::as_str), Some("my-lib"));
assert_eq!(parsed.package.get("version").map(String::as_str), Some("0.1.0"));
assert_eq!(parsed.workflows, vec!["auto-release", "pre-merge-gate", "security-gate"]);
assert!(parsed.ci_config.contains_key("bump"));
}
}