use crate::config::{BuildConfig, PluginEntry};
pub fn generate_cargo_toml(cfg: &BuildConfig) -> String {
let mut deps = String::new();
if let Some(path) = &cfg.build.folk_ext_path {
deps.push_str(&format!(
"folk-ext = {{ path = \"{}\", default-features = false }}\n",
path
));
} else {
deps.push_str("folk-ext = { version = \"0.2\", default-features = false }\n");
}
deps.push_str(
r#"folk-api = "0.2"
serde_json = "1"
toml = "0.8"
ext-php-rs = "0.15"
bytes = "1"
anyhow = "1"
"#,
);
for plugin in &cfg.plugin {
deps.push_str(&generate_plugin_dep(plugin));
}
let patch_section = if let Some(path) = &cfg.build.folk_api_path {
format!(
"\n[patch.crates-io]\nfolk-api = {{ path = \"{}\" }}\n",
path
)
} else {
String::new()
};
format!(
r#"[package]
name = "{output}"
version = "0.1.0"
edition = "2024"
[lib]
name = "{output}"
crate-type = ["cdylib"]
[dependencies]
{deps}
[build-dependencies]
ext-php-rs-build = "0.1"
{patch}"#,
output = cfg.build.output,
deps = deps,
patch = patch_section
)
}
fn generate_plugin_dep(p: &PluginEntry) -> String {
if let Some(path) = &p.path {
format!("{} = {{ path = \"{}\" }}\n", p.crate_name, path)
} else if let Some(git) = &p.git {
let ver = p.version.as_deref().unwrap_or("0.1");
format!(
"{} = {{ git = \"{}\", version = \"{}\" }}\n",
p.crate_name, git, ver
)
} else {
let ver = p.version.as_deref().unwrap_or("0.1");
format!("{} = \"{}\"\n", p.crate_name, ver)
}
}
pub fn generate_lib_rs(cfg: &BuildConfig) -> String {
let imports: Vec<String> = cfg
.plugin
.iter()
.map(|p| {
let ident = p.crate_name.replace('-', "_");
format!("use {}::folk_plugin_factory as {}_factory;", ident, ident)
})
.collect();
let registrations: Vec<String> = cfg
.plugin
.iter()
.map(|p| {
let ident = p.crate_name.replace('-', "_");
let key = if p.config_key.is_empty() {
&p.crate_name
} else {
&p.config_key
};
format!(
r#" let cfg_{ident} = raw_cfg.get("{key}").cloned().unwrap_or(toml::Value::Table(Default::default()));
let cfg_{ident}_json = serde_json::to_value(&cfg_{ident})?;
plugins.push({ident}_factory().create(cfg_{ident}_json)?);
"#,
)
})
.collect();
format!(
r#"//! Generated by folk-builder. Do not edit.
//! Plugins: {plugin_list}
use ext_php_rs::binary::Binary;
use ext_php_rs::prelude::*;
{imports}
fn create_plugins(config_path: &str) -> anyhow::Result<(folk_ext::folk_core::config::FolkConfig, Vec<Box<dyn folk_api::Plugin>>)> {{
let config = folk_ext::folk_core::config::FolkConfig::load_from(config_path)?;
let raw_cfg: toml::Table = {{
let content = std::fs::read_to_string(config_path).unwrap_or_default();
content.parse().unwrap_or_else(|_| toml::Table::new())
}};
let mut plugins: Vec<Box<dyn folk_api::Plugin>> = Vec::new();
{registrations}
Ok((config, plugins))
}}
// --- PHP wrappers ---
#[php_class]
#[php(name = "Folk\\Server")]
#[derive(Debug)]
pub struct FolkServer {{
config_path: String,
}}
#[php_impl]
impl FolkServer {{
pub fn __construct(config_path: String) -> Self {{
Self {{ config_path }}
}}
pub fn start(&self) -> PhpResult<()> {{
let (config, plugins) = create_plugins(&self.config_path)
.map_err(|e| PhpException::default(format!("Config error: {{e}}")))?;
folk_ext::start_server(config, plugins)
.map_err(|e| PhpException::default(format!("Start error: {{e}}")))?;
Ok(())
}}
}}
#[php_function]
pub fn folk_version() -> String {{
folk_ext::version()
}}
#[php_function]
pub fn folk_call(method: String, payload: Binary<u8>) -> PhpResult<Binary<u8>> {{
let data: Vec<u8> = payload.into();
let result = folk_ext::call_method(&method, bytes::Bytes::from(data))
.map_err(|e| PhpException::default(format!("folk_call({{method}}): {{e}}")))?;
Ok(Binary::new(result.to_vec()))
}}
#[php_function]
pub fn folk_worker_ready() -> PhpResult<bool> {{
folk_ext::bridge::do_ready()
.map_err(|e| PhpException::default(format!("folk_worker_ready: {{e}}")))
}}
#[php_function]
pub fn folk_worker_recv() -> PhpResult<Option<Vec<Binary<u8>>>> {{
match folk_ext::bridge::do_recv() {{
Ok(Some((method, payload))) => {{
Ok(Some(vec![Binary::new(method.into_bytes()), Binary::new(payload)]))
}},
Ok(None) => Ok(None),
Err(e) => Err(PhpException::default(format!("folk_worker_recv: {{e}}"))),
}}
}}
#[php_function]
pub fn folk_worker_send(result: Binary<u8>) -> PhpResult<()> {{
let data: Vec<u8> = result.into();
folk_ext::bridge::do_send(&data)
.map_err(|e| PhpException::default(format!("folk_worker_send: {{e}}")))
}}
#[php_function]
pub fn folk_worker_send_error(message: String) -> PhpResult<()> {{
folk_ext::bridge::do_send_error(&message)
.map_err(|e| PhpException::default(format!("folk_worker_send_error: {{e}}")))
}}
#[php_function]
pub fn folk_is_worker_thread() -> bool {{
folk_ext::bridge::has_worker_state()
}}
#[php_function]
pub fn folk_worker_run(dispatch_fn: String) -> PhpResult<()> {{
folk_ext::bridge::run_dispatch_loop(&dispatch_fn)
.map_err(|e| PhpException::default(format!("folk_worker_run: {{e}}")))
}}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{
module
.class::<FolkServer>()
.function(wrap_function!(folk_version))
.function(wrap_function!(folk_call))
.function(wrap_function!(folk_worker_ready))
.function(wrap_function!(folk_worker_recv))
.function(wrap_function!(folk_worker_send))
.function(wrap_function!(folk_worker_send_error))
.function(wrap_function!(folk_is_worker_thread))
.function(wrap_function!(folk_worker_run))
}}
"#,
imports = imports.join("\n"),
registrations = registrations.join(""),
plugin_list = cfg
.plugin
.iter()
.map(|p| p.crate_name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
}
pub fn generate_build_rs() -> String {
r#"use ext_php_rs_build::{ApiVersion, PHPInfo, emit_check_cfg, emit_php_cfg_flags, find_php};
fn main() {
let php = find_php().expect("Failed to find PHP");
let info = PHPInfo::get(&php).expect("Failed to get PHP info");
let version: ApiVersion = info
.zend_version()
.expect("Failed to get Zend version")
.try_into()
.expect("Unsupported PHP version");
emit_php_cfg_flags(version);
emit_check_cfg();
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os == "macos" {
println!("cargo:rustc-cdylib-link-arg=-undefined");
println!("cargo:rustc-cdylib-link-arg=dynamic_lookup");
}
}
"#
.to_string()
}