main
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}