vivacity_core/
runtime_stub.rs1use crate::error::{Error, Result};
11use serde_json::Value;
12use std::path::{Path, PathBuf};
13
14pub const AUTOLOAD_RUNTIME_TEMPLATE: &str = r#"<?php
15
16// autoload_runtime.php @generated by Symfony Runtime
17
18if (true === (require_once __DIR__.'/autoload.php') || empty($_SERVER['SCRIPT_FILENAME'])) {
19 return;
20}
21
22$app = require $_SERVER['SCRIPT_FILENAME'];
23
24if (!is_object($app)) {
25 throw new TypeError(sprintf('Invalid return value: callable object expected, "%s" returned from "%s".', get_debug_type($app), $_SERVER['SCRIPT_FILENAME']));
26}
27
28if (is_string($_SERVER['APP_RUNTIME_OPTIONS'] ??= $_ENV['APP_RUNTIME_OPTIONS'] ?? [])) {
29 $_SERVER['APP_RUNTIME_OPTIONS'] = json_decode($_SERVER['APP_RUNTIME_OPTIONS'], true, 512, JSON_THROW_ON_ERROR);
30}
31$_SERVER['APP_RUNTIME'] ??= $_ENV['APP_RUNTIME'] ?? %runtime_class%;
32$runtime = new $_SERVER['APP_RUNTIME']($_SERVER['APP_RUNTIME_OPTIONS'] += %runtime_options%);
33
34[$app, $args] = $runtime
35 ->getResolver($app)
36 ->resolve();
37
38$app = $app(...$args);
39
40exit(
41 $runtime
42 ->getRunner($app)
43 ->run()
44);
45"#;
46
47pub fn write_stub(vendor_dir: &Path, project_dir: &Path, root_manifest: &Value) -> Result<()> {
54 let extra = root_manifest.get("extra").and_then(|e| e.get("runtime"));
55 if extra == Some(&Value::Bool(false)) {
56 return Ok(());
57 }
58 let mut options: serde_json::Map<String, Value> = match extra {
59 Some(Value::Object(m)) => m.clone(),
60 _ => serde_json::Map::new(),
61 };
62 let template = match options.get("autoload_template").and_then(Value::as_str) {
63 Some(t) => {
64 let path = if crate::pathutil::is_absolute_path(t) {
65 PathBuf::from(t)
66 } else {
67 project_dir.join(t)
68 };
69 std::fs::read_to_string(&path).map_err(|_| {
70 Error::Unsupported(format!(
71 "File \"{t}\" defined under \"extra.runtime.autoload_template\" in your composer.json file not found."
72 ))
73 })?
74 }
75 None => {
76 let shipped = vendor_dir.join("symfony/runtime/Internal/autoload_runtime.template");
77 std::fs::read_to_string(&shipped)
78 .unwrap_or_else(|_| AUTOLOAD_RUNTIME_TEMPLATE.to_owned())
79 }
80 };
81 let vendor_real =
84 std::fs::canonicalize(vendor_dir).unwrap_or_else(|_| vendor_dir.to_path_buf());
85 let sub = options
86 .get("project_dir")
87 .and_then(Value::as_str)
88 .unwrap_or("");
89 let target =
90 std::fs::canonicalize(project_dir.join(sub)).unwrap_or_else(|_| project_dir.join(sub));
91 let mut relative = make_path_relative(&target, &vendor_real);
92 let mut nesting = 0;
93 while let Some(rest) = relative.strip_prefix("../") {
94 nesting += 1;
95 relative = rest.to_owned();
96 }
97 let project_code = if nesting == 0 {
98 format!("__DIR__.{}", php_var_export_str(&format!("/{relative}")))
99 } else if relative.is_empty() {
100 format!("dirname(__DIR__, {nesting})")
101 } else {
102 format!(
103 "dirname(__DIR__, {nesting}).{}",
104 php_var_export_str(&format!("/{relative}"))
105 )
106 };
107 let class = options
108 .get("class")
109 .and_then(Value::as_str)
110 .unwrap_or("Symfony\\Component\\Runtime\\SymfonyRuntime")
111 .to_owned();
112 for k in ["class", "autoload_template", "project_dir"] {
113 options.remove(k);
114 }
115 let exported = php_var_export(&Value::Object(options), 0);
117 let inner = &exported[7..exported.len() - 1];
118 let runtime_options = format!("[{inner} 'project_dir' => {project_code},\n]");
119 let code = template
120 .replace("%project_dir%", &project_code)
121 .replace("%runtime_class%", &php_var_export_str(&class))
122 .replace("%runtime_options%", &runtime_options);
123 let path = vendor_dir.join("autoload_runtime.php");
124 if std::fs::read_to_string(&path).ok().as_deref() == Some(code.as_str()) {
125 return Ok(());
126 }
127 let tmp = vendor_dir.join(".autoload_runtime.php.vivacity-tmp");
128 std::fs::write(&tmp, code).map_err(Error::io(&tmp))?;
129 std::fs::rename(&tmp, &path).map_err(Error::io(&path))?;
130 Ok(())
131}
132
133fn make_path_relative(end: &Path, start: &Path) -> String {
137 let e: Vec<String> = end
138 .components()
139 .skip(1)
140 .map(|c| c.as_os_str().to_string_lossy().into_owned())
141 .collect();
142 let st: Vec<String> = start
143 .components()
144 .skip(1)
145 .map(|c| c.as_os_str().to_string_lossy().into_owned())
146 .collect();
147 let common = e.iter().zip(st.iter()).take_while(|(a, b)| a == b).count();
148 let mut out = "../".repeat(st.len() - common);
149 let rest = e[common..].join("/");
150 if !rest.is_empty() {
151 out.push_str(&rest);
152 out.push('/');
153 }
154 if out.is_empty() {
155 "./".to_owned()
156 } else {
157 out
158 }
159}
160
161fn php_var_export_str(s: &str) -> String {
163 format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'"))
164}
165
166pub fn php_var_export(v: &Value, indent: usize) -> String {
169 let pad = " ".repeat(indent);
170 match v {
171 Value::Null => "NULL".to_owned(),
172 Value::Bool(b) => b.to_string(),
173 Value::Number(n) => {
174 if let Some(i) = n.as_i64() {
175 i.to_string()
176 } else {
177 let f = n.as_f64().unwrap_or(0.0);
178 let s = crate::phpjson::php_double(f).unwrap_or_default();
179 if s.contains('.') || s.contains('E') || s.contains('e') {
180 s
181 } else {
182 format!("{s}.0")
183 }
184 }
185 }
186 Value::String(s) => php_var_export_str(s),
187 Value::Array(items) => {
188 let mut out = String::from("array (\n");
189 for (i, item) in items.iter().enumerate() {
190 out.push_str(&format!(
191 "{pad} {i} => {}",
192 php_var_export_nested(item, indent + 1)
193 ));
194 }
195 out.push_str(&format!("{pad})"));
196 out
197 }
198 Value::Object(map) => {
199 let mut out = String::from("array (\n");
200 for (k, item) in map {
201 let key = match k.parse::<i64>() {
202 Ok(i) if i.to_string() == *k => i.to_string(),
203 _ => php_var_export_str(k),
204 };
205 out.push_str(&format!(
206 "{pad} {key} => {}",
207 php_var_export_nested(item, indent + 1)
208 ));
209 }
210 out.push_str(&format!("{pad})"));
211 out
212 }
213 }
214}
215
216fn php_var_export_nested(v: &Value, indent: usize) -> String {
219 match v {
220 Value::Array(_) | Value::Object(_) => {
221 format!("\n{}{},\n", " ".repeat(indent), php_var_export(v, indent))
222 }
223 _ => format!("{},\n", php_var_export(v, indent)),
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use serde_json::json;
231
232 #[test]
233 fn var_export_like_php() {
234 assert_eq!(php_var_export(&json!({}), 0), "array (\n)");
236 assert_eq!(
237 php_var_export(&json!({"debug": true, "env": "prod", "n": 3, "list": ["a"], "sub": {"k": null}}), 0),
238 "array (\n 'debug' => true,\n 'env' => 'prod',\n 'n' => 3,\n 'list' => \n array (\n 0 => 'a',\n ),\n 'sub' => \n array (\n 'k' => NULL,\n ),\n)"
239 );
240 }
241
242 #[test]
243 fn default_stub_matches_the_observed_output() {
244 let tmp = tempfile::tempdir().expect("tmp");
245 let vendor = tmp.path().join("vendor");
246 std::fs::create_dir_all(&vendor).expect("mkdir");
247 write_stub(&vendor, tmp.path(), &json!({})).expect("stub");
248 let code = std::fs::read_to_string(vendor.join("autoload_runtime.php")).expect("read");
249 assert!(code.contains("$_SERVER['APP_RUNTIME'] ??= $_ENV['APP_RUNTIME'] ?? 'Symfony\\\\Component\\\\Runtime\\\\SymfonyRuntime';"));
250 assert!(code.contains(
251 "$_SERVER['APP_RUNTIME_OPTIONS'] += [\n 'project_dir' => dirname(__DIR__, 1),\n]);"
252 ));
253 write_stub(&vendor, tmp.path(), &json!({"extra": {"runtime": {"class": "App\\Runtime", "dotenv_path": ".env.local", "project_dir": "."}}})).expect("stub");
255 let code = std::fs::read_to_string(vendor.join("autoload_runtime.php")).expect("read");
256 assert!(code.contains("?? 'App\\\\Runtime';"), "{code}");
257 assert!(code.contains("+= [\n 'dotenv_path' => '.env.local',\n 'project_dir' => dirname(__DIR__, 1),\n]);"), "{code}");
258 }
259}