1use crate::prompt_bool;
  2use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
  3use regex::Regex;
  4use std::ffi::OsStr;
  5use std::fs;
  6use std::io::{BufReader, Read};
  7use std::os::unix::prelude::OsStrExt;
  8use std::path::PathBuf;
  9use std::process::{exit, Command, Stdio};
 10
 11/// Summary of actions that will be taken
 12#[derive(Debug, Default)]
 13pub struct Summary {
 14    pub root: PathBuf,
 15    /// E.g. `version: 1.8.8+50`
 16    pub new_version_line: Option<String>,
 17    pub update_flutter: bool,
 18    pub update_dependencies: bool,
 19    pub run_tests: bool,
 20    pub build: bool,
 21}
 22
 23impl Summary {
 24    pub fn print(&self) {
 25        println!("Summary:\n\
 26        > root = {}\n\
 27        > new_version_line = {:?}\n\
 28        > update_flutter = {}\n\
 29        > update_dependencies = {}\n\
 30        > run_tests = {}\n\
 31        > build = {}",
 32                &self.root.to_string_lossy(),
 33                &self.new_version_line,
 34                &self.update_flutter,
 35                &self.update_dependencies,
 36                &self.run_tests,
 37                &self.build);
 38    }
 39
 40    /// Apply this config to the file system.
 41    pub fn apply(&self) {
 42        // Order:
 43        // 1. update flutter
 44        // 2. Write new app and flutter verrsion to pubspec
 45        // 3. update dependencies
 46        // 4. run tests and build
 47        let spinner_style = ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}")
 48            .unwrap()
 49            .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ");
 50        let m = MultiProgress::new();
 51
 52        if self.update_flutter {
 53            let pb = m.add(ProgressBar::new_spinner());
 54            pb.set_style(spinner_style.clone());
 55            pb.set_prefix("$ flutter upgrade");
 56            pb.set_message("Starting...");
 57
 58            let child = Command::new("flutter")
 59                .arg("upgrade")
 60                .stdout(Stdio::piped())
 61                .spawn();
 62            if let Ok(mut child) = child {
 63                let mut stdout = child.stdout.take().unwrap();
 64                pb.set_message("Started");
 65                loop {
 66                    let mut buf = [0;255];
 67                    match stdout.read(&mut buf) {
 68                        Err(err) => {
 69                            println!("{}] Error reading from stream: {}", line!(), err);
 70                            break;
 71                        }
 72                        Ok(got) => {
 73                            if got == 0 {
 74                                break;
 75                            } else {
 76                                let msg = OsStr::from_bytes(&buf)
 77                                    .to_string_lossy();
 78                                pb.println(msg);
 79                            }
 80                        }
 81                    }
 82                }
 83                pb.set_message("Stopped reading from stdout");
 84                child.wait().unwrap();
 85                pb.finish_with_message("Completed");
 86            } else if let Err(err) = child {
 87                pb.println(format!("{}", err));
 88                pb.finish_with_message("flutter upgrade failed");
 89            }
 90        }
 91
 92        let current_flutter_version = {
 93            let pb = m.add(ProgressBar::new_spinner());
 94            pb.set_style(spinner_style.clone());
 95            pb.set_prefix("$ flutter --version");
 96            pb.set_message("Starting...");
 97            let ouput = Command::new("flutter").arg("--version").stdout(Stdio::piped()).output().unwrap();
 98            pb.set_message("Extracting version...");
 99            if !ouput.status.success() {
100                pb.println(format!("{}", String::from_utf8_lossy(&ouput.stderr)));
101                exit(1);
102            }
103            pb.finish_with_message("Completed");
104            let output = String::from_utf8_lossy(&ouput.stdout);
105            let version = output.split(" ").nth(1).unwrap();
106            version.to_string()
107        };
108
109        if self.update_dependencies {
110            let pb = m.add(ProgressBar::new_spinner());
111            pb.set_style(spinner_style.clone());
112            pb.set_prefix("Updating dependencies");
113
114            pb.set_message("health_data_store pub deps");
115            Self::spawn_propagating_logs(
116                Command::new("dart")
117                            .arg("pub").arg("upgrade")
118                            .arg("--tighten").arg("--major-versions")
119                            .current_dir(&self.root.join("health_data_store")),
120                    &pb,
121            );
122
123            pb.set_message("health_data_store generate");
124            Self::spawn_propagating_logs(
125                Command::new("dart")
126                            .arg("run").arg("build_runner").arg("build")
127                            .arg("--delete-conflicting-outputs")
128                            .current_dir(&self.root.join("health_data_store")),
129                    &pb,
130            );
131
132            pb.set_message("app pub deps");
133            Self::spawn_propagating_logs(
134                Command::new("flutter")
135                            .arg("pub").arg("upgrade")
136                            .arg("--tighten").arg("--major-versions")
137                            .current_dir(&self.root.join("app")),
138                    &pb,
139            );
140
141            pb.set_message("app generate");
142            Self::spawn_propagating_logs(
143                Command::new("flutter").arg("pub")
144                            .arg("run").arg("build_runner").arg("build")
145                            .arg("--delete-conflicting-outputs")
146                            .current_dir(&self.root.join("app")),
147                    &pb,
148            );
149
150            pb.finish_with_message("Completed");
151        }
152
153        if self.update_flutter || self.new_version_line.is_some() {
154            _ = m.println("Updating pubspec.yaml");
155            let pubspec = fs::read_to_string(self.root.join("app").join("pubspec.yaml")).unwrap();
156            let version_re = Regex::new(r"flutter: '\d*.\d*.\d*'").unwrap();
157            let pubspec = version_re.replace(pubspec.as_str(), format!("flutter: '{current_flutter_version}'"));
158
159            let pubspec = if let Some(new_version_line) = &self.new_version_line {
160                let version_re = Regex::new(r"version:\s*\d*.\d*.\d*\+\d*").unwrap();
161                version_re.replace(&pubspec, new_version_line)
162            } else { pubspec };
163
164            fs::write(self.root.join("app").join("pubspec.yaml"), pubspec.to_string()).unwrap();
165        }
166
167        if self.run_tests {
168            let pb = m.add(ProgressBar::new_spinner());
169            pb.set_style(spinner_style.clone());
170            pb.set_prefix("Running tests");
171
172            pb.set_message("Testing health_data_store");
173            let libs_ok = Self::spawn_propagating_logs(
174                Command::new("dart").arg("test")
175                    .current_dir(&self.root.join("health_data_store")),
176                &pb,
177            );
178
179            pb.set_message("Testing app");
180            let app_ok = Self::spawn_propagating_logs(
181                Command::new("flutter").arg("test")
182                    .current_dir(&self.root.join("app")),
183                &pb,
184            );
185
186            if !libs_ok || !app_ok {
187                if !prompt_bool("App or Library tests failed. Do you want to proceed?", None).unwrap_or(false) {
188                    exit(0);
189                }
190            }
191
192            pb.finish();
193        }
194
195        if self.build {
196            let pb = m.add(ProgressBar::new_spinner());
197            pb.set_style(spinner_style.clone());
198            pb.set_prefix("Build App");
199
200            pb.set_message("Cleaning...");
201            Self::spawn_propagating_logs(
202                Command::new("flutter").arg("clean")
203                        .current_dir(&self.root.join("app")),
204                    &pb,
205            );
206
207            pb.set_message("Build APK...");
208            let debug_info_path = self.root
209                .join("app")
210                .join("build")
211                .join("debug_info");
212            Self::spawn_propagating_logs(
213                Command::new("flutter").arg("build").arg("apk")
214                        .arg("--release").arg("--flavor").arg("github")
215                        .arg("--obfuscate").arg(format!("--split-debug-info={}", debug_info_path.display()))
216                        .current_dir(&self.root.join("app")),
217                    &pb,
218            );
219
220            pb.set_message("Build bundle...");
221            Self::spawn_propagating_logs(
222                Command::new("flutter").arg("build").arg("appbundle")
223                        .arg("--release").arg("--flavor").arg("github")
224                        .arg("--obfuscate").arg(format!("--split-debug-info={}", debug_info_path.display()))
225                        .current_dir(&self.root.join("app")),
226                    &pb,
227            );
228
229            pb.set_message("Compressing debug symbols");
230            Command::new("zip")
231                .arg("-r").arg("debug-info.zip").arg(".") // zip everything in debug_info
232                .current_dir(&debug_info_path)
233                .status().unwrap();
234
235            // Clean target dir and copy files
236            pb.set_message("Copying outputs");
237            let target_dir = self.root.join("target");
238            if target_dir.exists() {
239                fs::remove_dir_all(&target_dir).unwrap();
240            }
241            fs::create_dir_all(&target_dir).unwrap();
242            let out_base = self.root.join("app").join("build").join("app").join("outputs");
243            let apk_path = out_base.join("flutter-apk").join("app-github-release.apk");
244            let aab_path = out_base.join("bundle").join("githubRelease").join("app-github-release.aab");
245            let zip_path = debug_info_path.join("debug-info.zip");
246
247            fs::copy(&apk_path, target_dir.join("app-github-release.apk")).unwrap();
248            fs::copy(&aab_path, target_dir.join("app-github-release.aab")).unwrap();
249            fs::copy(&zip_path, target_dir.join("debug-info.zip")).unwrap();
250
251            pb.finish();
252        }
253    }
254
255     fn spawn_propagating_logs(cmd: &mut Command, pb: &ProgressBar) -> bool {
256        let child = cmd.stdout(Stdio::piped()).spawn();
257        if let Ok(mut child) = child {
258            let stdout = child.stdout.take().unwrap();
259            let mut reader = BufReader::new(stdout);
260            let mut line_buf: Vec<u8> = Vec::new();
261            let mut read_buf = [0; 1024];
262
263            while let Ok(n) = reader.read(&mut read_buf) {
264                if n == 0 {
265                    break;
266                }
267
268                for &byte in &read_buf[..n] {
269                    match byte {
270                        b'\n' | b'\r' => {
271                            pb.println(&String::from_utf8_lossy(&line_buf));
272                            pb.force_draw();
273                            line_buf.clear();
274                        }
275                        _ => {
276                            line_buf.push(byte);
277                        }
278                    }
279                }
280            }
281
282            if !line_buf.is_empty() {
283                pb.println(&String::from_utf8_lossy(&line_buf));
284            }
285
286            return child.wait().is_ok_and(|e| e.success())
287        }
288         false
289    }
290}