1use crate::config::{BuildConfig, PluginEntry};
2
3pub fn generate_cargo_toml(cfg: &BuildConfig) -> String {
4 let mut deps = String::new();
5
6 if let Some(path) = &cfg.build.folk_ext_path {
7 deps.push_str(&format!(
8 "folk-ext = {{ path = \"{}\", default-features = false }}\n",
9 path
10 ));
11 } else {
12 deps.push_str("folk-ext = { version = \"0.2\", default-features = false }\n");
13 }
14
15 deps.push_str(
16 r#"folk-api = "0.2"
17serde_json = "1"
18toml = "0.8"
19ext-php-rs = "0.15"
20bytes = "1"
21anyhow = "1"
22"#,
23 );
24
25 for plugin in &cfg.plugin {
26 deps.push_str(&generate_plugin_dep(plugin));
27 }
28
29 let patch_section = if let Some(path) = &cfg.build.folk_api_path {
30 format!(
31 "\n[patch.crates-io]\nfolk-api = {{ path = \"{}\" }}\n",
32 path
33 )
34 } else {
35 String::new()
36 };
37
38 format!(
39 r#"[package]
40name = "{output}"
41version = "0.1.0"
42edition = "2024"
43
44[lib]
45name = "{output}"
46crate-type = ["cdylib"]
47
48[dependencies]
49{deps}
50[build-dependencies]
51ext-php-rs-build = "0.1"
52{patch}"#,
53 output = cfg.build.output,
54 deps = deps,
55 patch = patch_section
56 )
57}
58
59fn generate_plugin_dep(p: &PluginEntry) -> String {
60 if let Some(path) = &p.path {
61 format!("{} = {{ path = \"{}\" }}\n", p.crate_name, path)
62 } else if let Some(git) = &p.git {
63 let ver = p.version.as_deref().unwrap_or("0.1");
64 format!(
65 "{} = {{ git = \"{}\", version = \"{}\" }}\n",
66 p.crate_name, git, ver
67 )
68 } else {
69 let ver = p.version.as_deref().unwrap_or("0.1");
70 format!("{} = \"{}\"\n", p.crate_name, ver)
71 }
72}
73
74pub fn generate_lib_rs(cfg: &BuildConfig) -> String {
75 let imports: Vec<String> = cfg
76 .plugin
77 .iter()
78 .map(|p| {
79 let ident = p.crate_name.replace('-', "_");
80 format!("use {}::folk_plugin_factory as {}_factory;", ident, ident)
81 })
82 .collect();
83
84 let registrations: Vec<String> = cfg
85 .plugin
86 .iter()
87 .map(|p| {
88 let ident = p.crate_name.replace('-', "_");
89 let key = if p.config_key.is_empty() {
90 &p.crate_name
91 } else {
92 &p.config_key
93 };
94 format!(
95 r#" let cfg_{ident} = raw_cfg.get("{key}").cloned().unwrap_or(toml::Value::Table(Default::default()));
96 let cfg_{ident}_json = serde_json::to_value(&cfg_{ident})?;
97 plugins.push({ident}_factory().create(cfg_{ident}_json)?);
98"#,
99 )
100 })
101 .collect();
102
103 format!(
104 r#"//! Generated by folk-builder. Do not edit.
105//! Plugins: {plugin_list}
106
107use ext_php_rs::binary::Binary;
108use ext_php_rs::prelude::*;
109{imports}
110
111fn create_plugins(config_path: &str) -> anyhow::Result<(folk_ext::folk_core::config::FolkConfig, Vec<Box<dyn folk_api::Plugin>>)> {{
112 let config = folk_ext::folk_core::config::FolkConfig::load_from(config_path)?;
113 let raw_cfg: toml::Table = {{
114 let content = std::fs::read_to_string(config_path).unwrap_or_default();
115 content.parse().unwrap_or_else(|_| toml::Table::new())
116 }};
117 let mut plugins: Vec<Box<dyn folk_api::Plugin>> = Vec::new();
118{registrations}
119 Ok((config, plugins))
120}}
121
122// --- PHP wrappers ---
123
124#[php_class]
125#[php(name = "Folk\\Server")]
126#[derive(Debug)]
127pub struct FolkServer {{
128 config_path: String,
129}}
130
131#[php_impl]
132impl FolkServer {{
133 pub fn __construct(config_path: String) -> Self {{
134 Self {{ config_path }}
135 }}
136
137 pub fn start(&self) -> PhpResult<()> {{
138 let (config, plugins) = create_plugins(&self.config_path)
139 .map_err(|e| PhpException::default(format!("Config error: {{e}}")))?;
140 folk_ext::start_server(config, plugins)
141 .map_err(|e| PhpException::default(format!("Start error: {{e}}")))?;
142 Ok(())
143 }}
144}}
145
146#[php_function]
147pub fn folk_version() -> String {{
148 folk_ext::version()
149}}
150
151#[php_function]
152pub fn folk_call(method: String, payload: Binary<u8>) -> PhpResult<Binary<u8>> {{
153 let data: Vec<u8> = payload.into();
154 let result = folk_ext::call_method(&method, bytes::Bytes::from(data))
155 .map_err(|e| PhpException::default(format!("folk_call({{method}}): {{e}}")))?;
156 Ok(Binary::new(result.to_vec()))
157}}
158
159#[php_function]
160pub fn folk_worker_ready() -> PhpResult<bool> {{
161 folk_ext::bridge::do_ready()
162 .map_err(|e| PhpException::default(format!("folk_worker_ready: {{e}}")))
163}}
164
165#[php_function]
166pub fn folk_worker_recv() -> PhpResult<Option<Vec<Binary<u8>>>> {{
167 match folk_ext::bridge::do_recv() {{
168 Ok(Some((method, payload))) => {{
169 Ok(Some(vec![Binary::new(method.into_bytes()), Binary::new(payload)]))
170 }},
171 Ok(None) => Ok(None),
172 Err(e) => Err(PhpException::default(format!("folk_worker_recv: {{e}}"))),
173 }}
174}}
175
176#[php_function]
177pub fn folk_worker_send(result: Binary<u8>) -> PhpResult<()> {{
178 let data: Vec<u8> = result.into();
179 folk_ext::bridge::do_send(&data)
180 .map_err(|e| PhpException::default(format!("folk_worker_send: {{e}}")))
181}}
182
183#[php_function]
184pub fn folk_worker_send_error(message: String) -> PhpResult<()> {{
185 folk_ext::bridge::do_send_error(&message)
186 .map_err(|e| PhpException::default(format!("folk_worker_send_error: {{e}}")))
187}}
188
189#[php_function]
190pub fn folk_is_worker_thread() -> bool {{
191 folk_ext::bridge::has_worker_state()
192}}
193
194#[php_function]
195pub fn folk_worker_run(dispatch_fn: String) -> PhpResult<()> {{
196 folk_ext::bridge::run_dispatch_loop(&dispatch_fn)
197 .map_err(|e| PhpException::default(format!("folk_worker_run: {{e}}")))
198}}
199
200#[php_module]
201pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{
202 module
203 .class::<FolkServer>()
204 .function(wrap_function!(folk_version))
205 .function(wrap_function!(folk_call))
206 .function(wrap_function!(folk_worker_ready))
207 .function(wrap_function!(folk_worker_recv))
208 .function(wrap_function!(folk_worker_send))
209 .function(wrap_function!(folk_worker_send_error))
210 .function(wrap_function!(folk_is_worker_thread))
211 .function(wrap_function!(folk_worker_run))
212}}
213"#,
214 imports = imports.join("\n"),
215 registrations = registrations.join(""),
216 plugin_list = cfg
217 .plugin
218 .iter()
219 .map(|p| p.crate_name.as_str())
220 .collect::<Vec<_>>()
221 .join(", ")
222 )
223}
224
225pub fn generate_build_rs() -> String {
226 r#"use ext_php_rs_build::{ApiVersion, PHPInfo, emit_check_cfg, emit_php_cfg_flags, find_php};
227
228fn main() {
229 let php = find_php().expect("Failed to find PHP");
230 let info = PHPInfo::get(&php).expect("Failed to get PHP info");
231 let version: ApiVersion = info
232 .zend_version()
233 .expect("Failed to get Zend version")
234 .try_into()
235 .expect("Unsupported PHP version");
236 emit_php_cfg_flags(version);
237 emit_check_cfg();
238
239 let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
240 if target_os == "macos" {
241 println!("cargo:rustc-cdylib-link-arg=-undefined");
242 println!("cargo:rustc-cdylib-link-arg=dynamic_lookup");
243 }
244}
245"#
246 .to_string()
247}