stellar_scaffold_cli/commands/
clean.rs1use crate::commands::build::clients::ScaffoldEnv;
2use crate::commands::build::env_toml::{self, Account, Environment};
3use cargo_metadata::Metadata;
4use clap::Parser;
5use std::{
6 fs, io,
7 path::{Path, PathBuf},
8 process::Command,
9};
10use stellar_cli::{commands::global, print::Print};
11#[derive(Parser, Debug, Clone)]
13pub struct Cmd {
14 #[arg(long)]
16 pub manifest_path: Option<PathBuf>,
17}
18
19#[derive(thiserror::Error, Debug)]
20pub enum Error {
21 #[error(transparent)]
22 IO(#[from] io::Error),
23
24 #[error("network config is not sufficient: need name or url and passphrase")]
25 NetworkConfig,
26
27 #[error(transparent)]
28 EnvToml(#[from] env_toml::Error),
29}
30
31impl Cmd {
43 pub fn run(&self, global_args: &global::Args) -> Result<(), Error> {
44 let printer = Print::new(global_args.quiet);
45 printer.infoln("Starting workspace cleanup");
46
47 let cargo_meta = match &self.manifest_path {
48 Some(manifest_path) => cargo_metadata::MetadataCommand::new()
49 .manifest_path(manifest_path)
50 .no_deps()
51 .exec()
52 .unwrap(),
53 _ => cargo_metadata::MetadataCommand::new()
54 .no_deps()
55 .exec()
56 .unwrap(),
57 };
58
59 Self::clean_target_stellar(&cargo_meta, &printer)?;
60
61 let workspace_root: PathBuf = cargo_meta.workspace_root.into();
62
63 Self::clean_packages(&workspace_root, &printer)?;
64
65 Self::clean_src_contracts(&workspace_root, &printer)?;
66
67 Self::clean_contract_aliases(&workspace_root, &printer)?;
68
69 Self::clean_identities(&workspace_root, &printer);
70
71 Ok(())
72 }
73
74 fn clean_target_stellar(cargo_meta: &Metadata, printer: &Print) -> Result<(), Error> {
75 let target_dir = &cargo_meta.target_directory;
76 let stellar_dir = target_dir.join("stellar");
77 if stellar_dir.exists() {
78 fs::remove_dir_all(&stellar_dir)?;
79 } else {
80 printer.infoln(format!(
81 "Skipping target clean: {stellar_dir} does not exist"
82 ));
83 }
84 Ok(())
85 }
86
87 fn clean_packages(workspace_root: &Path, printer: &Print) -> Result<(), Error> {
88 let packages_path: PathBuf = workspace_root.join("packages");
89 let git_tracked_packages_entries = Self::git_tracked_entries(workspace_root, "packages");
90 Self::clean_dir(
91 workspace_root,
92 &packages_path,
93 &git_tracked_packages_entries,
94 printer,
95 )
96 }
97
98 fn clean_src_contracts(workspace_root: &Path, printer: &Print) -> Result<(), Error> {
99 let src_contracts_path = workspace_root.join("src").join("contracts");
100 let git_tracked_src_contract_entries =
101 Self::git_tracked_entries(workspace_root, "src/contracts");
102 Self::clean_dir(
103 workspace_root,
104 &src_contracts_path,
105 &git_tracked_src_contract_entries,
106 printer,
107 )
108 }
109
110 fn clean_contract_aliases(workspace_root: &Path, printer: &Print) -> Result<(), Error> {
111 match Environment::get(workspace_root, &ScaffoldEnv::Development) {
112 Ok(Some(env)) => {
113 let network_args = Self::get_network_args(&env)?;
114 if let Some(contracts) = &env.contracts {
115 for (contract_name, _) in contracts {
116 let result = std::process::Command::new("stellar")
117 .args(["contract", "alias", "remove", contract_name])
118 .args(&network_args)
119 .output();
120
121 match result {
122 Ok(output) if output.status.success() => {
123 printer.infoln(format!("Removed contract alias: {contract_name}"));
124 }
125 Ok(output) => {
126 let stderr = String::from_utf8_lossy(&output.stderr);
127 if !stderr.contains("not found") && !stderr.contains("No alias") {
128 printer.warnln(format!(
129 "Failed to remove contract alias {contract_name}: {stderr}"
130 ));
131 }
132 }
133 Err(e) => {
134 printer.warnln(format!(
135 "Failed to execute stellar contract alias remove: {e}"
136 ));
137 }
138 }
139 }
140 }
141 }
142 Ok(None) => {
143 printer.infoln("No development environment found in environments.toml");
144 }
145 Err(e) => {
146 printer.warnln(format!("Failed to read environments.toml: {e}"));
147 }
148 }
149
150 Ok(())
151 }
152
153 fn clean_identities(workspace_root: &Path, printer: &Print) {
154 match Environment::get(workspace_root, &ScaffoldEnv::Development) {
155 Ok(Some(env)) => {
156 if let Some(accounts) = &env.accounts {
158 for account in accounts {
159 let other_envs = Self::account_in_other_envs(workspace_root, account);
160 if !other_envs.is_empty() {
161 printer.warnln(format!("Skipping cleaning identity {:?}. It is being used in other environments: {:?}.", account.name, other_envs));
162 return;
163 }
164
165 let result = std::process::Command::new("stellar")
166 .args(["keys", "rm", &account.name])
167 .output();
168
169 match result {
170 Ok(output) if output.status.success() => {
171 printer.infoln(format!("Removed account: {}", &account.name));
172 }
173 Ok(output) => {
174 let stderr = String::from_utf8_lossy(&output.stderr);
175 if !stderr.contains("not found") && !stderr.contains("No alias") {
176 printer.warnln(format!(
177 "Warning: Failed to remove account {}: {stderr}",
178 &account.name
179 ));
180 }
181 }
182 Err(e) => {
183 printer.warnln(format!(" Warning: Failed to execute stellar contract alias remove: {e}"));
184 }
185 }
186 }
187 }
188 }
189 Ok(None) => {
190 printer.infoln("No development environment found in environments.toml");
191 }
192 Err(e) => {
193 printer.warnln(format!("Warning: Failed to read environments.toml: {e}"));
194 }
195 }
196 }
197
198 fn account_in_other_envs(workspace_root: &Path, current_account: &Account) -> Vec<ScaffoldEnv> {
199 let mut other_envs: Vec<ScaffoldEnv> = vec![];
200
201 if let Some(testing) = Environment::get(workspace_root, &ScaffoldEnv::Testing)
202 .ok()
203 .flatten()
204 {
205 let found = testing
206 .accounts
207 .as_ref()
208 .is_some_and(|accts| accts.iter().any(|acct| acct.name == current_account.name));
209 if found {
210 other_envs.push(ScaffoldEnv::Testing);
211 }
212 }
213
214 if let Some(staging) = Environment::get(workspace_root, &ScaffoldEnv::Staging)
215 .ok()
216 .flatten()
217 {
218 let found = staging
219 .accounts
220 .as_ref()
221 .is_some_and(|accts| accts.iter().any(|acct| acct.name == current_account.name));
222 if found {
223 other_envs.push(ScaffoldEnv::Staging);
224 }
225 }
226
227 if let Some(production) = Environment::get(workspace_root, &ScaffoldEnv::Production)
228 .ok()
229 .flatten()
230 {
231 let found = production
232 .accounts
233 .as_ref()
234 .is_some_and(|accts| accts.iter().any(|acct| acct.name == current_account.name));
235 if found {
236 other_envs.push(ScaffoldEnv::Production);
237 }
238 }
239
240 other_envs
241 }
242
243 fn get_network_args(env: &Environment) -> Result<Vec<&str>, Error> {
244 match (
245 &env.network.name,
246 &env.network.rpc_url,
247 &env.network.network_passphrase,
248 ) {
249 (Some(name), _, _) => Ok(vec!["--network", name]),
250 (None, Some(url), Some(passphrase)) => {
251 Ok(vec!["--rpc-url", url, "--network-passphrase", passphrase])
252 }
253 _ => Err(Error::NetworkConfig),
254 }
255 }
256
257 fn git_tracked_entries(workspace_root: &Path, subdir: &str) -> Vec<String> {
258 let output = Command::new("git")
259 .args(["ls-files", subdir])
260 .current_dir(workspace_root)
261 .output();
262
263 match output {
264 Ok(output) if output.status.success() => {
265 let stdout = String::from_utf8_lossy(&output.stdout);
266 stdout
267 .lines()
268 .map(std::string::ToString::to_string)
269 .collect()
270 }
271 _ => {
272 Vec::new()
274 }
275 }
276 }
277
278 fn clean_dir(
280 workspace_root: &Path,
281 dir_to_clean: &Path,
282 git_tracked_entries: &[String],
283 printer: &Print,
284 ) -> Result<(), Error> {
285 if dir_to_clean.exists() {
286 for entry in fs::read_dir(dir_to_clean)? {
287 let entry = entry?;
288 let path = entry.path();
289 let relative_path = path.strip_prefix(workspace_root).unwrap_or(&path);
290 let relative_str = relative_path.to_string_lossy().replace('\\', "/");
291
292 if git_tracked_entries.contains(&relative_str) {
294 continue;
295 }
296
297 let filename = path.file_name().and_then(|n| n.to_str());
299 if let Some(name) = filename
300 && (name == "util.ts" || name == ".gitkeep")
301 {
302 continue;
303 }
304
305 if path.is_dir() {
307 fs::remove_dir_all(&path).unwrap();
308 } else {
309 fs::remove_file(&path).unwrap();
310 }
311 printer.infoln(format!("Removed {relative_str}"));
312 }
313 } else {
314 printer.infoln(format!(
315 "Skipping clean: {} does not exist",
316 dir_to_clean.display()
317 ));
318 }
319
320 Ok(())
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use std::path::Path;
328 use tempfile::TempDir;
329
330 fn create_test_workspace(temp_dir: &Path) -> PathBuf {
331 let manifest_path = temp_dir.join("Cargo.toml");
332 fs::write(
333 &manifest_path,
334 r#"[package]
335name = "soroban-hello-world-contract"
336version = "0.0.0"
337edition = "2021"
338publish = false
339
340[lib]
341crate-type = ["cdylib"]
342"#,
343 )
344 .unwrap();
345
346 let src_dir = temp_dir.join("src");
347 fs::create_dir_all(&src_dir).unwrap();
348 fs::write(src_dir.join("lib.rs"), "// dummy lib").unwrap();
349
350 manifest_path
351 }
352
353 #[test]
354 fn test_clean_target_stellar() {
355 let global_args = global::Args::default();
356 let temp_dir = TempDir::new().unwrap();
357 let manifest_path = create_test_workspace(temp_dir.path());
358
359 let target_stellar_path = temp_dir.path().join("target").join("stellar");
360 std::fs::create_dir_all(&target_stellar_path).unwrap();
361
362 let cmd = Cmd {
363 manifest_path: Some(manifest_path),
364 };
365 assert!(cmd.run(&global_args).is_ok());
366
367 assert!(
368 !target_stellar_path.exists(),
369 "target/stellar should be removed"
370 );
371 }
372
373 #[test]
374 fn test_clean_packages() {
375 let global_args = global::Args::default();
376 let temp_dir = TempDir::new().unwrap();
377 let manifest_path = create_test_workspace(temp_dir.path());
378
379 let packages_path = temp_dir.path().join("packages");
380 let test_package_path = packages_path.join("test_contract_package");
381 std::fs::create_dir_all(&test_package_path).unwrap();
382
383 let gitkeep_path = packages_path.join(".gitkeep");
384 fs::write(&gitkeep_path, "").unwrap();
385
386 let cmd = Cmd {
387 manifest_path: Some(manifest_path),
388 };
389
390 assert!(cmd.run(&global_args).is_ok());
391
392 assert!(
393 !test_package_path.exists(),
394 "packages/test_contract_package/ should be removed"
395 );
396 assert!(
397 gitkeep_path.exists(),
398 "packages/.gitkeep should be preserved"
399 );
400 }
401
402 #[test]
403 fn test_clean_src_contracts() {
404 let global_args = global::Args::default();
405 let temp_dir = TempDir::new().unwrap();
406 let manifest_path = create_test_workspace(temp_dir.path());
407
408 let src_contracts_path = temp_dir.path().join("src").join("contracts");
409 std::fs::create_dir_all(&src_contracts_path).unwrap();
410
411 let test_contract_path = src_contracts_path.join("test_contract_client.js");
412 fs::write(&test_contract_path, "").unwrap();
413
414 let util_path = src_contracts_path.join("util.ts");
415 fs::write(&util_path, "").unwrap();
416
417 let cmd = Cmd {
418 manifest_path: Some(manifest_path),
419 };
420
421 assert!(cmd.run(&global_args).is_ok());
422
423 assert!(
424 !test_contract_path.exists(),
425 "src/contracts/test_contract_client.js should be removed"
426 );
427 assert!(
428 util_path.exists(),
429 "src/contracts/util.js should be preserved"
430 );
431 }
432}