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 es_fluent_derive_core::get_es_fluent_temp_dir;
13use std::path::{Path, PathBuf};
14use std::process::Command;
15use std::{env, fs};
16
17const CLI_VERSION: &str = env!("CARGO_PKG_VERSION");
18
19pub struct TempCrateConfig {
26 pub es_fluent_dep: String,
27 pub es_fluent_cli_helpers_dep: String,
28 pub target_dir: String,
29}
30
31impl TempCrateConfig {
32 pub fn from_manifest(manifest_path: &Path) -> Self {
34 use super::cache::MetadataCache;
35
36 let target_dir_from_env = std::env::var("CARGO_TARGET_DIR").ok();
38
39 let workspace_root = manifest_path.parent().unwrap_or(Path::new("."));
41 let temp_dir = get_es_fluent_temp_dir(workspace_root);
42
43 if let Some(cache) = MetadataCache::load(&temp_dir)
45 && cache.is_valid(workspace_root)
46 {
47 return Self {
48 es_fluent_dep: cache.es_fluent_dep,
49 es_fluent_cli_helpers_dep: cache.es_fluent_cli_helpers_dep,
50 target_dir: target_dir_from_env.unwrap_or(cache.target_dir),
51 };
52 }
53
54 let metadata = cargo_metadata::MetadataCommand::new()
57 .manifest_path(manifest_path)
58 .no_deps()
59 .exec()
60 .ok();
61
62 let (es_fluent_dep, es_fluent_cli_helpers_dep, target_dir) = match metadata {
63 Some(ref meta) => {
64 let es_fluent = Self::find_local_dep(meta, "es-fluent")
65 .or_else(Self::find_cli_workspace_dep_es_fluent)
66 .unwrap_or_else(|| format!(r#"es-fluent = {{ version = "{}" }}"#, CLI_VERSION));
67 let helpers = Self::find_local_dep(meta, "es-fluent-cli-helpers")
68 .or_else(Self::find_cli_workspace_dep_helpers)
69 .unwrap_or_else(|| {
70 format!(
71 r#"es-fluent-cli-helpers = {{ version = "{}" }}"#,
72 CLI_VERSION
73 )
74 });
75 let target = target_dir_from_env
76 .clone()
77 .unwrap_or_else(|| meta.target_directory.to_string());
78 (es_fluent, helpers, target)
79 },
80 None => (
81 Self::find_cli_workspace_dep_es_fluent()
82 .unwrap_or_else(|| format!(r#"es-fluent = {{ version = "{}" }}"#, CLI_VERSION)),
83 Self::find_cli_workspace_dep_helpers().unwrap_or_else(|| {
84 format!(
85 r#"es-fluent-cli-helpers = {{ version = "{}" }}"#,
86 CLI_VERSION
87 )
88 }),
89 target_dir_from_env
90 .clone()
91 .unwrap_or_else(|| "../target".to_string()),
92 ),
93 };
94
95 if let Some(cargo_lock_hash) = MetadataCache::hash_cargo_lock(workspace_root) {
97 let _ = std::fs::create_dir_all(&temp_dir);
98 let cache = MetadataCache {
99 cargo_lock_hash,
100 es_fluent_dep: es_fluent_dep.clone(),
101 es_fluent_cli_helpers_dep: es_fluent_cli_helpers_dep.clone(),
102 target_dir: target_dir.clone(),
103 };
104 let _ = cache.save(&temp_dir);
105 }
106
107 Self {
108 es_fluent_dep,
109 es_fluent_cli_helpers_dep,
110 target_dir,
111 }
112 }
113
114 fn find_local_dep(meta: &cargo_metadata::Metadata, crate_name: &str) -> Option<String> {
115 meta.packages
116 .iter()
117 .find(|p| p.name.as_str() == crate_name && p.source.is_none())
118 .map(|pkg| {
119 let path = pkg.manifest_path.parent().unwrap();
120 format!(r#"{} = {{ path = "{}" }}"#, crate_name, path)
121 })
122 }
123
124 fn find_cli_workspace_dep_es_fluent() -> Option<String> {
126 let cli_manifest_dir = env!("CARGO_MANIFEST_DIR");
127 let cli_path = Path::new(cli_manifest_dir);
128 let es_fluent_path = cli_path.parent()?.join("es-fluent");
129 if es_fluent_path.join("Cargo.toml").exists() {
130 Some(format!(
131 r#"es-fluent = {{ path = "{}" }}"#,
132 es_fluent_path.display()
133 ))
134 } else {
135 None
136 }
137 }
138
139 fn find_cli_workspace_dep_helpers() -> Option<String> {
141 let cli_manifest_dir = env!("CARGO_MANIFEST_DIR");
142 let cli_path = Path::new(cli_manifest_dir);
143 let helpers_path = cli_path.parent()?.join("es-fluent-cli-helpers");
144 if helpers_path.join("Cargo.toml").exists() {
145 Some(format!(
146 r#"es-fluent-cli-helpers = {{ path = "{}" }}"#,
147 helpers_path.display()
148 ))
149 } else {
150 None
151 }
152 }
153}
154
155struct RunnerCrate<'a> {
156 temp_dir: &'a Path,
157}
158
159impl RunnerCrate<'_> {
160 fn new(temp_dir: &Path) -> RunnerCrate<'_> {
161 RunnerCrate { temp_dir }
162 }
163
164 fn manifest_path(&self) -> PathBuf {
165 self.temp_dir.join("Cargo.toml")
166 }
167
168 fn write_cargo_toml(&self, cargo_toml_content: &str) -> Result<()> {
170 fs::write(self.temp_dir.join("Cargo.toml"), cargo_toml_content)
171 .context("Failed to write .es-fluent/Cargo.toml")
172 }
173
174 fn write_cargo_config(&self, config_content: &str) -> Result<()> {
176 let cargo_dir = self.temp_dir.join(".cargo");
177 fs::create_dir_all(&cargo_dir).context("Failed to create .es-fluent/.cargo directory")?;
178 fs::write(cargo_dir.join("config.toml"), config_content)
179 .context("Failed to write .es-fluent/.cargo/config.toml")
180 }
181
182 fn run_cargo(&self, bin_name: Option<&str>, args: &[String]) -> Result<String> {
186 let mut cmd = Command::new("cargo");
187 cmd.arg("run");
188 if let Some(bin) = bin_name {
189 cmd.arg("--bin").arg(bin);
190 }
191 cmd.arg("--manifest-path")
192 .arg(self.manifest_path())
193 .arg("--quiet")
194 .arg("--")
195 .args(args)
196 .current_dir(self.temp_dir)
197 .env("RUSTFLAGS", "-A warnings");
198
199 if std::env::var("NO_COLOR").is_err() {
201 cmd.env("CLICOLOR_FORCE", "1");
202 }
203
204 let output = cmd.output().context("Failed to run cargo")?;
206
207 if !output.status.success() {
208 let stderr = String::from_utf8_lossy(&output.stderr);
209 bail!("Cargo run failed: {}", stderr)
210 }
211
212 Ok(String::from_utf8_lossy(&output.stdout).to_string())
213 }
214
215 fn run_cargo_with_output(
219 &self,
220 bin_name: Option<&str>,
221 args: &[String],
222 ) -> Result<std::process::Output> {
223 let mut cmd = Command::new("cargo");
224 cmd.arg("run");
225 if let Some(bin) = bin_name {
226 cmd.arg("--bin").arg(bin);
227 }
228 cmd.arg("--manifest-path")
229 .arg(self.manifest_path())
230 .arg("--quiet")
231 .arg("--") .args(args)
233 .current_dir(self.temp_dir)
234 .env("RUSTFLAGS", "-A warnings");
235
236 let output = cmd.output().context("Failed to run cargo")?;
237
238 if output.status.success() {
239 Ok(output)
240 } else {
241 let stderr = String::from_utf8_lossy(&output.stderr);
242 bail!("Cargo run failed: {}", stderr)
243 }
244 }
245}
246
247pub fn run_cargo(temp_dir: &Path, bin_name: Option<&str>, args: &[String]) -> Result<String> {
251 RunnerCrate::new(temp_dir).run_cargo(bin_name, args)
252}
253
254pub fn run_cargo_with_output(
258 temp_dir: &Path,
259 bin_name: Option<&str>,
260 args: &[String],
261) -> Result<std::process::Output> {
262 RunnerCrate::new(temp_dir).run_cargo_with_output(bin_name, args)
263}
264
265use crate::core::WorkspaceInfo;
268
269struct MonolithicRunner<'a> {
270 workspace: &'a WorkspaceInfo,
271 temp_dir: PathBuf,
272 binary_path: PathBuf,
273}
274
275impl<'a> MonolithicRunner<'a> {
276 fn new(workspace: &'a WorkspaceInfo) -> Self {
277 Self {
278 workspace,
279 temp_dir: get_es_fluent_temp_dir(&workspace.root_dir),
280 binary_path: get_monolithic_binary_path(workspace),
281 }
282 }
283
284 fn is_stale(&self) -> bool {
285 use super::cache::{RunnerCache, compute_content_hash};
286
287 let runner_mtime = match fs::metadata(&self.binary_path).and_then(|m| m.modified()) {
288 Ok(t) => t,
289 Err(_) => return true, };
291
292 let runner_mtime_secs = runner_mtime
293 .duration_since(std::time::SystemTime::UNIX_EPOCH)
294 .map(|d| d.as_secs())
295 .unwrap_or(0);
296
297 let mut current_hashes = indexmap::IndexMap::new();
299 for krate in &self.workspace.crates {
300 if krate.src_dir.exists() {
301 let hash = compute_content_hash(&krate.src_dir, Some(&krate.i18n_config_path));
302 current_hashes.insert(krate.name.clone(), hash);
303 }
304 }
305
306 if let Some(cache) = RunnerCache::load(&self.temp_dir) {
308 if cache.cli_version != CLI_VERSION {
310 return true;
311 }
312
313 if cache.runner_mtime == runner_mtime_secs {
314 for (name, current_hash) in ¤t_hashes {
316 match cache.crate_hashes.get(name) {
317 Some(cached_hash) if cached_hash == current_hash => continue,
318 _ => return true, }
320 }
321 for cached_name in cache.crate_hashes.keys() {
323 if !current_hashes.contains_key(cached_name) {
324 return true;
325 }
326 }
327 return false;
329 }
330
331 let new_cache = RunnerCache {
333 crate_hashes: current_hashes,
334 runner_mtime: runner_mtime_secs,
335 cli_version: CLI_VERSION.to_string(),
336 };
337 let _ = new_cache.save(&self.temp_dir);
338 return false;
339 }
340
341 true
343 }
344}
345
346pub fn prepare_monolithic_runner_crate(workspace: &WorkspaceInfo) -> Result<PathBuf> {
349 let temp_dir = get_es_fluent_temp_dir(&workspace.root_dir);
350 let src_dir = temp_dir.join("src");
351 let runner_crate = RunnerCrate::new(&temp_dir);
352
353 fs::create_dir_all(&src_dir).context("Failed to create .es-fluent directory")?;
354
355 fs::write(
357 temp_dir.join(".gitignore"),
358 GitignoreTemplate.render().unwrap(),
359 )
360 .context("Failed to write .es-fluent/.gitignore")?;
361
362 let root_manifest = workspace.root_dir.join("Cargo.toml");
364 let config = TempCrateConfig::from_manifest(&root_manifest);
365
366 let crate_deps: Vec<MonolithicCrateDep> = workspace
368 .crates
369 .iter()
370 .filter(|c| c.has_lib_rs) .map(|c| MonolithicCrateDep {
372 name: &c.name,
373 path: c.manifest_dir.display().to_string(),
374 ident: c.name.replace('-', "_"),
375 has_features: !c.fluent_features.is_empty(),
376 features: &c.fluent_features,
377 })
378 .collect();
379
380 let cargo_toml = MonolithicCargoTomlTemplate {
382 crates: crate_deps.clone(),
383 es_fluent_dep: &config.es_fluent_dep,
384 es_fluent_cli_helpers_dep: &config.es_fluent_cli_helpers_dep,
385 };
386 runner_crate.write_cargo_toml(&cargo_toml.render().unwrap())?;
387
388 let config_toml = ConfigTomlTemplate {
390 target_dir: &config.target_dir,
391 };
392 runner_crate.write_cargo_config(&config_toml.render().unwrap())?;
393
394 let main_rs = MonolithicMainRsTemplate { crates: crate_deps };
396 fs::write(src_dir.join("main.rs"), main_rs.render().unwrap())
397 .context("Failed to write .es-fluent/src/main.rs")?;
398
399 let workspace_lock = workspace.root_dir.join("Cargo.lock");
403 let runner_lock = temp_dir.join("Cargo.lock");
404 if workspace_lock.exists() {
405 fs::copy(&workspace_lock, &runner_lock)
406 .context("Failed to copy Cargo.lock to .es-fluent/")?;
407 }
408
409 Ok(temp_dir)
410}
411
412pub fn get_monolithic_binary_path(workspace: &WorkspaceInfo) -> PathBuf {
414 workspace.target_dir.join("debug").join("es-fluent-runner")
415}
416
417pub fn run_monolithic(
421 workspace: &WorkspaceInfo,
422 command: &str,
423 crate_name: &str,
424 extra_args: &[String],
425 force_run: bool,
426) -> Result<String> {
427 let runner = MonolithicRunner::new(workspace);
428
429 if !force_run && runner.binary_path.exists() && !runner.is_stale() {
431 let mut cmd = Command::new(&runner.binary_path);
432 cmd.arg(command)
433 .args(extra_args) .arg("--crate")
435 .arg(crate_name)
436 .current_dir(&runner.temp_dir);
437
438 if std::env::var("NO_COLOR").is_err() {
440 cmd.env("CLICOLOR_FORCE", "1");
441 }
442
443 let output = cmd.output().context("Failed to run monolithic binary")?;
444
445 if !output.status.success() {
446 let stderr = String::from_utf8_lossy(&output.stderr);
447 bail!("Monolithic binary failed: {}", stderr);
448 }
449
450 return Ok(String::from_utf8_lossy(&output.stdout).to_string());
451 }
452
453 let mut args = vec![command.to_string()];
456 args.extend(extra_args.iter().cloned());
457 args.push("--crate".to_string());
458 args.push(crate_name.to_string());
459 let result = RunnerCrate::new(&runner.temp_dir).run_cargo(Some("es-fluent-runner"), &args)?;
460
461 {
463 use super::cache::{RunnerCache, compute_content_hash};
464
465 if let Ok(meta) = fs::metadata(&runner.binary_path)
466 && let Ok(mtime) = meta.modified()
467 {
468 let runner_mtime_secs = mtime
469 .duration_since(std::time::SystemTime::UNIX_EPOCH)
470 .map(|d| d.as_secs())
471 .unwrap_or(0);
472
473 let mut crate_hashes = indexmap::IndexMap::new();
475 for krate in &workspace.crates {
476 if krate.src_dir.exists() {
477 let hash = compute_content_hash(&krate.src_dir, Some(&krate.i18n_config_path));
478 crate_hashes.insert(krate.name.clone(), hash);
479 }
480 }
481
482 let cache = RunnerCache {
483 crate_hashes,
484 runner_mtime: runner_mtime_secs,
485 cli_version: CLI_VERSION.to_string(),
486 };
487 let _ = cache.save(&runner.temp_dir);
488 }
489 }
490
491 Ok(result)
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use std::io::Write;
498
499 #[test]
500 fn test_temp_crate_config_nonexistent_manifest() {
501 let config = TempCrateConfig::from_manifest(Path::new("/nonexistent/Cargo.toml"));
502 assert!(config.es_fluent_dep.contains("es-fluent"));
505 }
506
507 #[test]
508 fn test_temp_crate_config_non_workspace_member() {
509 let temp_dir = tempfile::tempdir().unwrap();
510 let manifest_path = temp_dir.path().join("Cargo.toml");
511
512 let cargo_toml = r#"
513[package]
514name = "test-crate"
515version = "0.1.0"
516edition = "2024"
517
518[dependencies]
519es-fluent = { version = "*" }
520"#;
521 let mut file = fs::File::create(&manifest_path).unwrap();
522 file.write_all(cargo_toml.as_bytes()).unwrap();
523
524 let src_dir = temp_dir.path().join("src");
525 fs::create_dir_all(&src_dir).unwrap();
526 fs::write(src_dir.join("lib.rs"), "").unwrap();
527
528 let config = TempCrateConfig::from_manifest(&manifest_path);
529 assert!(config.es_fluent_dep.contains("es-fluent"));
531 }
532}