es_fluent_cli/generation/
runner.rs1use crate::generation::templates::{
7 ConfigTomlTemplate, GitignoreTemplate, MonolithicCargoTomlTemplate, MonolithicCrateDep,
8 MonolithicMainRsTemplate,
9};
10use anyhow::{Context as _, Result, bail};
11use askama::Template as _;
12use std::path::{Path, PathBuf};
13use std::process::Command;
14use std::{env, fs};
15
16pub const TEMP_DIR: &str = ".es-fluent";
17
18const CLI_VERSION: &str = env!("CARGO_PKG_VERSION");
19
20pub struct TempCrateConfig {
27 pub es_fluent_dep: String,
28 pub es_fluent_cli_helpers_dep: String,
29 pub target_dir: String,
30}
31
32impl TempCrateConfig {
33 pub fn from_manifest(manifest_path: &Path) -> Self {
35 use super::cache::MetadataCache;
36
37 let target_dir_from_env = std::env::var("CARGO_TARGET_DIR").ok();
39
40 let workspace_root = manifest_path.parent().unwrap_or(Path::new("."));
42 let temp_dir = workspace_root.join(TEMP_DIR);
43
44 if let Some(cache) = MetadataCache::load(&temp_dir)
46 && cache.is_valid(workspace_root)
47 {
48 return Self {
49 es_fluent_dep: cache.es_fluent_dep,
50 es_fluent_cli_helpers_dep: cache.es_fluent_cli_helpers_dep,
51 target_dir: target_dir_from_env.unwrap_or(cache.target_dir),
52 };
53 }
54
55 let metadata = cargo_metadata::MetadataCommand::new()
58 .manifest_path(manifest_path)
59 .no_deps()
60 .exec()
61 .ok();
62
63 let (es_fluent_dep, es_fluent_cli_helpers_dep, target_dir) = match metadata {
64 Some(ref meta) => {
65 let es_fluent = Self::find_local_dep(meta, "es-fluent")
66 .or_else(Self::find_cli_workspace_dep_es_fluent)
67 .unwrap_or_else(|| format!(r#"es-fluent = {{ version = "{}" }}"#, CLI_VERSION));
68 let helpers = Self::find_local_dep(meta, "es-fluent-cli-helpers")
69 .or_else(Self::find_cli_workspace_dep_helpers)
70 .unwrap_or_else(|| {
71 format!(
72 r#"es-fluent-cli-helpers = {{ version = "{}" }}"#,
73 CLI_VERSION
74 )
75 });
76 let target = target_dir_from_env
77 .clone()
78 .unwrap_or_else(|| meta.target_directory.to_string());
79 (es_fluent, helpers, target)
80 },
81 None => (
82 Self::find_cli_workspace_dep_es_fluent()
83 .unwrap_or_else(|| format!(r#"es-fluent = {{ version = "{}" }}"#, CLI_VERSION)),
84 Self::find_cli_workspace_dep_helpers().unwrap_or_else(|| {
85 format!(
86 r#"es-fluent-cli-helpers = {{ version = "{}" }}"#,
87 CLI_VERSION
88 )
89 }),
90 target_dir_from_env
91 .clone()
92 .unwrap_or_else(|| "../target".to_string()),
93 ),
94 };
95
96 if let Some(cargo_lock_hash) = MetadataCache::hash_cargo_lock(workspace_root) {
98 let _ = std::fs::create_dir_all(&temp_dir);
99 let cache = MetadataCache {
100 cargo_lock_hash,
101 es_fluent_dep: es_fluent_dep.clone(),
102 es_fluent_cli_helpers_dep: es_fluent_cli_helpers_dep.clone(),
103 target_dir: target_dir.clone(),
104 };
105 let _ = cache.save(&temp_dir);
106 }
107
108 Self {
109 es_fluent_dep,
110 es_fluent_cli_helpers_dep,
111 target_dir,
112 }
113 }
114
115 fn find_local_dep(meta: &cargo_metadata::Metadata, crate_name: &str) -> Option<String> {
116 meta.packages
117 .iter()
118 .find(|p| p.name.as_str() == crate_name && p.source.is_none())
119 .map(|pkg| {
120 let path = pkg.manifest_path.parent().unwrap();
121 format!(r#"{} = {{ path = "{}" }}"#, crate_name, path)
122 })
123 }
124
125 fn find_cli_workspace_dep_es_fluent() -> Option<String> {
127 let cli_manifest_dir = env!("CARGO_MANIFEST_DIR");
128 let cli_path = Path::new(cli_manifest_dir);
129 let es_fluent_path = cli_path.parent()?.join("es-fluent");
130 if es_fluent_path.join("Cargo.toml").exists() {
131 Some(format!(
132 r#"es-fluent = {{ path = "{}" }}"#,
133 es_fluent_path.display()
134 ))
135 } else {
136 None
137 }
138 }
139
140 fn find_cli_workspace_dep_helpers() -> Option<String> {
142 let cli_manifest_dir = env!("CARGO_MANIFEST_DIR");
143 let cli_path = Path::new(cli_manifest_dir);
144 let helpers_path = cli_path.parent()?.join("es-fluent-cli-helpers");
145 if helpers_path.join("Cargo.toml").exists() {
146 Some(format!(
147 r#"es-fluent-cli-helpers = {{ path = "{}" }}"#,
148 helpers_path.display()
149 ))
150 } else {
151 None
152 }
153 }
154}
155
156fn write_cargo_toml(temp_dir: &Path, cargo_toml_content: &str) -> Result<()> {
158 fs::write(temp_dir.join("Cargo.toml"), cargo_toml_content)
159 .context("Failed to write .es-fluent/Cargo.toml")
160}
161
162fn write_cargo_config(temp_dir: &Path, config_content: &str) -> Result<()> {
164 let cargo_dir = temp_dir.join(".cargo");
165 fs::create_dir_all(&cargo_dir).context("Failed to create .es-fluent/.cargo directory")?;
166 fs::write(cargo_dir.join("config.toml"), config_content)
167 .context("Failed to write .es-fluent/.cargo/config.toml")
168}
169
170pub fn run_cargo(temp_dir: &Path, bin_name: Option<&str>, args: &[String]) -> Result<String> {
174 let manifest_path = temp_dir.join("Cargo.toml");
175
176 let mut cmd = Command::new("cargo");
177 cmd.arg("run");
178 if let Some(bin) = bin_name {
179 cmd.arg("--bin").arg(bin);
180 }
181 cmd.arg("--manifest-path")
182 .arg(&manifest_path)
183 .arg("--quiet")
184 .arg("--")
185 .args(args)
186 .current_dir(temp_dir)
187 .env("RUSTFLAGS", "-A warnings");
188
189 if std::env::var("NO_COLOR").is_err() {
191 cmd.env("CLICOLOR_FORCE", "1");
192 }
193
194 let output = cmd.output().context("Failed to run cargo")?;
196
197 if !output.status.success() {
198 let stderr = String::from_utf8_lossy(&output.stderr);
199 bail!("Cargo run failed: {}", stderr)
200 }
201
202 Ok(String::from_utf8_lossy(&output.stdout).to_string())
203}
204
205pub fn run_cargo_with_output(
209 temp_dir: &Path,
210 bin_name: Option<&str>,
211 args: &[String],
212) -> Result<std::process::Output> {
213 let manifest_path = temp_dir.join("Cargo.toml");
214
215 let mut cmd = Command::new("cargo");
216 cmd.arg("run");
217 if let Some(bin) = bin_name {
218 cmd.arg("--bin").arg(bin);
219 }
220 cmd.arg("--manifest-path")
221 .arg(&manifest_path)
222 .arg("--quiet")
223 .arg("--") .args(args)
225 .current_dir(temp_dir)
226 .env("RUSTFLAGS", "-A warnings");
227
228 let output = cmd.output().context("Failed to run cargo")?;
229
230 if output.status.success() {
231 Ok(output)
232 } else {
233 let stderr = String::from_utf8_lossy(&output.stderr);
234 bail!("Cargo run failed: {}", stderr)
235 }
236}
237
238use crate::core::WorkspaceInfo;
241
242pub fn prepare_monolithic_runner_crate(workspace: &WorkspaceInfo) -> Result<PathBuf> {
245 let temp_dir = workspace.root_dir.join(TEMP_DIR);
246 let src_dir = temp_dir.join("src");
247
248 fs::create_dir_all(&src_dir).context("Failed to create .es-fluent directory")?;
249
250 fs::write(
252 temp_dir.join(".gitignore"),
253 GitignoreTemplate.render().unwrap(),
254 )
255 .context("Failed to write .es-fluent/.gitignore")?;
256
257 let root_manifest = workspace.root_dir.join("Cargo.toml");
259 let config = TempCrateConfig::from_manifest(&root_manifest);
260
261 let crate_deps: Vec<MonolithicCrateDep> = workspace
263 .crates
264 .iter()
265 .filter(|c| c.has_lib_rs) .map(|c| MonolithicCrateDep {
267 name: &c.name,
268 path: c.manifest_dir.display().to_string(),
269 ident: c.name.replace('-', "_"),
270 has_features: !c.fluent_features.is_empty(),
271 features: &c.fluent_features,
272 })
273 .collect();
274
275 let cargo_toml = MonolithicCargoTomlTemplate {
277 crates: crate_deps.clone(),
278 es_fluent_dep: &config.es_fluent_dep,
279 es_fluent_cli_helpers_dep: &config.es_fluent_cli_helpers_dep,
280 };
281 write_cargo_toml(&temp_dir, &cargo_toml.render().unwrap())?;
282
283 let config_toml = ConfigTomlTemplate {
285 target_dir: &config.target_dir,
286 };
287 write_cargo_config(&temp_dir, &config_toml.render().unwrap())?;
288
289 let main_rs = MonolithicMainRsTemplate { crates: crate_deps };
291 fs::write(src_dir.join("main.rs"), main_rs.render().unwrap())
292 .context("Failed to write .es-fluent/src/main.rs")?;
293
294 let workspace_lock = workspace.root_dir.join("Cargo.lock");
298 let runner_lock = temp_dir.join("Cargo.lock");
299 if workspace_lock.exists() {
300 fs::copy(&workspace_lock, &runner_lock)
301 .context("Failed to copy Cargo.lock to .es-fluent/")?;
302 }
303
304 Ok(temp_dir)
305}
306
307pub fn get_monolithic_binary_path(workspace: &WorkspaceInfo) -> PathBuf {
309 workspace.target_dir.join("debug").join("es-fluent-runner")
310}
311
312pub fn run_monolithic(
316 workspace: &WorkspaceInfo,
317 command: &str,
318 crate_name: &str,
319 extra_args: &[String],
320 force_run: bool,
321) -> Result<String> {
322 let temp_dir = workspace.root_dir.join(TEMP_DIR);
323 let binary_path = get_monolithic_binary_path(workspace);
324
325 if !force_run && binary_path.exists() && !is_runner_stale(workspace, &binary_path) {
327 let mut cmd = Command::new(&binary_path);
328 cmd.arg(command)
329 .args(extra_args) .arg("--crate")
331 .arg(crate_name)
332 .current_dir(&temp_dir);
333
334 if std::env::var("NO_COLOR").is_err() {
336 cmd.env("CLICOLOR_FORCE", "1");
337 }
338
339 let output = cmd.output().context("Failed to run monolithic binary")?;
340
341 if !output.status.success() {
342 let stderr = String::from_utf8_lossy(&output.stderr);
343 bail!("Monolithic binary failed: {}", stderr);
344 }
345
346 return Ok(String::from_utf8_lossy(&output.stdout).to_string());
347 }
348
349 let mut args = vec![command.to_string()];
352 args.extend(extra_args.iter().cloned());
353 args.push("--crate".to_string());
354 args.push(crate_name.to_string());
355 let result = run_cargo(&temp_dir, Some("es-fluent-runner"), &args)?;
356
357 {
359 use super::cache::{RunnerCache, compute_content_hash};
360
361 let binary_path = get_monolithic_binary_path(workspace);
362 if let Ok(meta) = fs::metadata(&binary_path)
363 && let Ok(mtime) = meta.modified()
364 {
365 let runner_mtime_secs = mtime
366 .duration_since(std::time::SystemTime::UNIX_EPOCH)
367 .map(|d| d.as_secs())
368 .unwrap_or(0);
369
370 let mut crate_hashes = indexmap::IndexMap::new();
372 for krate in &workspace.crates {
373 if krate.src_dir.exists() {
374 let hash = compute_content_hash(&krate.src_dir, Some(&krate.i18n_config_path));
375 crate_hashes.insert(krate.name.clone(), hash);
376 }
377 }
378
379 let cache = RunnerCache {
380 crate_hashes,
381 runner_mtime: runner_mtime_secs,
382 cli_version: CLI_VERSION.to_string(),
383 };
384 let _ = cache.save(&temp_dir);
385 }
386 }
387
388 Ok(result)
389}
390
391fn is_runner_stale(workspace: &WorkspaceInfo, runner_path: &Path) -> bool {
399 use super::cache::{RunnerCache, compute_content_hash};
400
401 let runner_mtime = match fs::metadata(runner_path).and_then(|m| m.modified()) {
402 Ok(t) => t,
403 Err(_) => return true, };
405
406 let runner_mtime_secs = runner_mtime
407 .duration_since(std::time::SystemTime::UNIX_EPOCH)
408 .map(|d| d.as_secs())
409 .unwrap_or(0);
410
411 let temp_dir = workspace.root_dir.join(TEMP_DIR);
412
413 let mut current_hashes = indexmap::IndexMap::new();
415 for krate in &workspace.crates {
416 if krate.src_dir.exists() {
417 let hash = compute_content_hash(&krate.src_dir, Some(&krate.i18n_config_path));
418 current_hashes.insert(krate.name.clone(), hash);
419 }
420 }
421
422 if let Some(cache) = RunnerCache::load(&temp_dir) {
424 if cache.cli_version != CLI_VERSION {
426 return true;
427 }
428
429 if cache.runner_mtime == runner_mtime_secs {
430 for (name, current_hash) in ¤t_hashes {
432 match cache.crate_hashes.get(name) {
433 Some(cached_hash) if cached_hash == current_hash => continue,
434 _ => return true, }
436 }
437 for cached_name in cache.crate_hashes.keys() {
439 if !current_hashes.contains_key(cached_name) {
440 return true;
441 }
442 }
443 return false;
445 }
446
447 let new_cache = RunnerCache {
449 crate_hashes: current_hashes,
450 runner_mtime: runner_mtime_secs,
451 cli_version: CLI_VERSION.to_string(),
452 };
453 let _ = new_cache.save(&temp_dir);
454 return false;
455 }
456
457 true
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use std::io::Write;
465
466 #[test]
467 fn test_temp_crate_config_nonexistent_manifest() {
468 let config = TempCrateConfig::from_manifest(Path::new("/nonexistent/Cargo.toml"));
469 assert!(config.es_fluent_dep.contains("es-fluent"));
472 }
473
474 #[test]
475 fn test_temp_crate_config_non_workspace_member() {
476 let temp_dir = tempfile::tempdir().unwrap();
477 let manifest_path = temp_dir.path().join("Cargo.toml");
478
479 let cargo_toml = r#"
480[package]
481name = "test-crate"
482version = "0.1.0"
483edition = "2024"
484
485[dependencies]
486es-fluent = { version = "*" }
487"#;
488 let mut file = fs::File::create(&manifest_path).unwrap();
489 file.write_all(cargo_toml.as_bytes()).unwrap();
490
491 let src_dir = temp_dir.path().join("src");
492 fs::create_dir_all(&src_dir).unwrap();
493 fs::write(src_dir.join("lib.rs"), "").unwrap();
494
495 let config = TempCrateConfig::from_manifest(&manifest_path);
496 assert!(config.es_fluent_dep.contains("es-fluent"));
498 }
499}