1use std::io::Write;
4use std::pin::Pin;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7
8use sva_ast::variable;
9use sva_core::solve::{
10 Point, Problem, Solved, Stopped, Vary, cost_of, findings, point_json, solve as solved,
11 solved_data,
12};
13use sva_core::{CliError, error_envelope_with, success_envelope};
14use sva_engine::Abandon;
15
16use crate::args::SolveArgs;
17use crate::heard::Heard;
18use crate::store::{On, opened, persisted, wait};
19
20pub const CANCELLED: u8 = 130;
22
23struct Flagged(Arc<AtomicBool>);
24
25impl Abandon for Flagged {
26 fn abandoned(&self) -> Pin<Box<dyn Future<Output = bool> + '_>> {
27 Box::pin(async { self.0.load(Ordering::Relaxed) })
28 }
29}
30
31pub fn solve(args: &SolveArgs) -> Result<(String, u8), CliError> {
33 let dir = crate::composition::composition()?;
34 let heard = Heard::at(&dir)?;
35 let stop = Arc::new(AtomicBool::new(false));
36 let mut warnings = Vec::new();
37 let tier = opened(&args.cache, &mut warnings, On::Flag(&stop))?;
38 let problem = Problem {
39 source: &heard,
40 texts: &heard,
41 cost: cost_of(&heard, &args.cost)?,
42 vary: args.vary.clone(),
43 sets: args.sets.clone(),
44 rate: args.rate,
45 threads: args.threads,
46 stops: args.stops,
47 };
48 let mut heard_point = |point: &Point| {
49 let _ = writeln!(std::io::stderr(), "{}", point_json(point));
51 };
52 let outcome = wait(solved(&problem, tier, &mut heard_point, &Flagged(stop)));
53 persisted(tier, &mut warnings);
54 let solved = outcome?;
55 warnings.extend(findings(&solved, &args.vary));
56 let stopped = solved.stopped_by == Stopped::Cancelled;
57 if stopped {
58 let data = solved_data(&solved, &args.vary, "");
59 let message = format!(
60 "the solve was stopped by a signal after {} render(s); `data` holds the best so far, \
61 and no file was written",
62 solved.renders
63 );
64 let json = error_envelope_with("cancelled", &message, &data, &warnings);
65 return Ok((json, CANCELLED));
66 }
67 if !args.write {
68 let data = solved_data(&solved, &args.vary, "");
69 return Ok((success_envelope(&data, &warnings), 0));
70 }
71 let changes = changed(&dir, &solved, &args.vary)?;
72 let listed = sva_core::json::list(&changes, |c| {
73 format!(
74 "{{ \"path\": \"{}\", \"from\": \"{}\", \"to\": \"{}\" }}",
75 sva_core::json::escape(&c.path),
76 sva_core::json::escape(&c.from),
77 sva_core::json::escape(&c.to)
78 )
79 });
80 if !changes.is_empty() && !args.confirm {
81 let data = solved_data(&solved, &args.vary, &format!(", \"would_write\": {listed}"));
82 let message = format!(
83 "`--write` would change {} variable file(s), and replacing a file needs \
84 `--confirm`; nothing was written",
85 changes.len()
86 );
87 let json = error_envelope_with("conflict", &message, &data, &warnings);
88 return Ok((json, 4));
89 }
90 for change in &changes {
91 std::fs::write(dir.join(&change.path), &change.text)
92 .map_err(|e| CliError::Io(format!("could not write `{}`: {e}", change.path)))?;
93 }
94 let data = solved_data(&solved, &args.vary, &format!(", \"written\": {listed}"));
95 Ok((success_envelope(&data, &warnings), 0))
96}
97
98struct Change {
99 path: String,
100 from: String,
101 to: String,
102 text: String,
103}
104
105fn changed(dir: &std::path::Path, solved: &Solved, vary: &[Vary]) -> Result<Vec<Change>, CliError> {
107 let mut out = Vec::new();
108 for ((name, _, to), _) in solved.best.values.iter().zip(vary) {
109 let path = variable(name);
110 let text = std::fs::read_to_string(dir.join(&path))
111 .map_err(|e| CliError::Io(format!("could not read `{path}`: {e}")))?;
112 let at = sva_ast::body_at(&text);
113 let body = &text[at..];
114 let from = sva_ast::strip_line_comment(body).trim().to_string();
115 if from == *to {
116 continue;
117 }
118 let comment = body.find(';').map(|c| format!(" {}", body[c..].trim_end()));
119 out.push(Change {
120 text: format!("{}{to}{}\n", &text[..at], comment.unwrap_or_default()),
121 path,
122 from,
123 to: to.clone(),
124 });
125 }
126 Ok(out)
127}