stellar_scaffold_cli/commands/
upgrade.rs1use crate::arg_parsing::ArgParser;
2use crate::commands::build::env_toml::{Account, Contract, Environment, Network};
3use clap::Parser;
4use degit::degit;
5use indexmap::IndexMap;
6use std::fs;
7use std::fs::{create_dir_all, metadata, read_dir, write};
8use std::io;
9use std::path::{Path, PathBuf};
10use stellar_cli::commands::global::Args;
11use toml_edit::{DocumentMut, Item, Table, value};
12
13use crate::{arg_parsing, commands::build, commands::init::FRONTEND_TEMPLATE};
14use stellar_cli::print::Print;
15
16#[derive(Parser, Debug, Clone)]
18pub struct Cmd {
19 #[arg(default_value = ".")]
21 pub workspace_path: PathBuf,
22 #[arg(long)]
24 pub skip_prompt: bool,
25}
26
27#[derive(thiserror::Error, Debug)]
29pub enum Error {
30 #[error("Failed to clone template: {0}")]
31 DegitError(String),
32 #[error("Workspace path contains invalid UTF-8 characters and cannot be converted to a string")]
33 InvalidWorkspacePathEncoding,
34 #[error("IO error: {0}")]
35 IoError(#[from] io::Error),
36 #[error("No Cargo.toml found in workspace path")]
37 NoCargoToml,
38 #[error("No contracts/ directory found in workspace path")]
39 NoContractsDirectory,
40 #[error("Invalid package name in Cargo.toml")]
41 InvalidPackageName,
42 #[error("Failed to parse TOML: {0}")]
43 TomlParseError(#[from] toml_edit::TomlError),
44 #[error("Failed to serialize TOML: {0}")]
45 TomlSerializeError(#[from] toml::ser::Error),
46 #[error("Failed to deserialize TOML: {0}")]
47 TomlDeserializeError(#[from] toml::de::Error),
48 #[error(transparent)]
49 BuildError(#[from] build::Error),
50 #[error("Failed to get constructor arguments: {0:?}")]
51 ConstructorArgsError(arg_parsing::Error),
52 #[error("WASM file not found for contract '{0}'. Please build the contract first.")]
53 WasmFileNotFound(String),
54 #[error(transparent)]
55 Clap(#[from] clap::Error),
56 #[error(transparent)]
57 SorobanSpecTools(#[from] soroban_spec_tools::contract::Error),
58 #[error(transparent)]
59 CopyError(#[from] fs_extra::error::Error),
60}
61
62impl Cmd {
63 pub async fn run(
72 &self,
73 global_args: &stellar_cli::commands::global::Args,
74 ) -> Result<(), Error> {
75 let printer = Print::new(global_args.quiet);
76
77 printer.infoln(format!(
78 "Upgrading Soroban workspace to scaffold project in {}",
79 self.workspace_path.display()
80 ));
81
82 self.validate_workspace()?;
84
85 let temp_dir = tempfile::tempdir().map_err(Error::IoError)?;
87 let temp_path = temp_dir.path();
88
89 printer.infoln("Downloading frontend template...");
90 Self::clone_frontend_template(temp_path)?;
91
92 printer.infoln("Copying frontend files...");
93 self.copy_frontend_files(temp_path)?;
94
95 printer.infoln("Setting up environment file...");
96 self.setup_env_file()?;
97
98 printer.infoln("Creating environments.toml...");
99 self.create_environments_toml(global_args).await?;
100
101 printer.checkln(format!(
102 "Workspace successfully upgraded to scaffold project at {}",
103 self.workspace_path.display()
104 ));
105
106 Ok(())
107 }
108
109 fn validate_workspace(&self) -> Result<(), Error> {
110 let cargo_toml = self.workspace_path.join("Cargo.toml");
112 if !cargo_toml.exists() {
113 return Err(Error::NoCargoToml);
114 }
115
116 let contracts_dir = self.workspace_path.join("contracts");
118 if !contracts_dir.exists() {
119 return Err(Error::NoContractsDirectory);
120 }
121
122 Ok(())
123 }
124
125 fn clone_frontend_template(temp_path: &Path) -> Result<(), Error> {
126 let temp_str = temp_path
127 .to_str()
128 .ok_or(Error::InvalidWorkspacePathEncoding)?;
129
130 degit(FRONTEND_TEMPLATE, temp_str);
131
132 if metadata(temp_path).is_err() || read_dir(temp_path)?.next().is_none() {
133 return Err(Error::DegitError(format!(
134 "Failed to clone template into {temp_str}: directory is empty or missing",
135 )));
136 }
137
138 Ok(())
139 }
140
141 fn copy_frontend_files(&self, temp_path: &Path) -> Result<(), Error> {
142 let skip_items = ["contracts", "environments.toml", "Cargo.toml"];
144
145 for entry in read_dir(temp_path)? {
147 let entry = entry?;
148 let item_name = entry.file_name();
149
150 if let Some(name_str) = item_name.to_str()
152 && skip_items.contains(&name_str)
153 {
154 continue;
155 }
156
157 let src = entry.path();
158 let dest = self.workspace_path.join(&item_name);
159
160 if dest.exists() {
162 continue;
163 }
164
165 if src.is_dir() {
166 let copy_options = fs_extra::dir::CopyOptions::new()
167 .overwrite(false) .skip_exist(true); fs_extra::dir::copy(&src, &self.workspace_path, ©_options)?;
171 } else {
172 let copy_options = fs_extra::file::CopyOptions::new().overwrite(false); fs_extra::file::copy(&src, &dest, ©_options)?;
175 }
176 }
177
178 let packages_dir = self.workspace_path.join("packages");
180 if !packages_dir.exists() {
181 create_dir_all(&packages_dir)?;
182 }
183
184 Ok(())
185 }
186
187 async fn create_environments_toml(
188 &self,
189 global_args: &stellar_cli::commands::global::Args,
190 ) -> Result<(), Error> {
191 let env_path = self.workspace_path.join("environments.toml");
192
193 if env_path.exists() {
195 return Ok(());
196 }
197
198 let contracts = self.discover_contracts(global_args)?;
200
201 self.build_contracts(global_args).await?;
203
204 let contract_configs = contracts
206 .iter()
207 .map(|contract_name| {
208 let constructor_args = self.get_constructor_args(contract_name)?;
209 Ok((
210 contract_name.clone().into_boxed_str(),
211 Contract {
212 constructor_args,
213 ..Default::default()
214 },
215 ))
216 })
217 .collect::<Result<IndexMap<_, _>, Error>>()?;
218
219 let env_config = Environment {
220 accounts: Some(vec![Account {
221 name: "default".to_string(),
222 default: true,
223 }]),
224 network: Network {
225 name: None,
226 rpc_url: Some("http://localhost:8000/rpc".to_string()),
227 network_passphrase: Some("Standalone Network ; February 2017".to_string()),
228 rpc_headers: None,
229 run_locally: true,
230 },
231 contracts: (!contract_configs.is_empty()).then_some(contract_configs),
232 extensions: vec![],
233 };
234
235 let mut doc = DocumentMut::new();
236
237 let mut dev_table = Table::new();
239
240 let mut accounts_array = toml_edit::Array::new();
242 accounts_array.push("default");
243 dev_table["accounts"] = Item::Value(accounts_array.into());
244
245 let mut network_table = Table::new();
247 network_table["rpc-url"] = value(env_config.network.rpc_url.as_ref().unwrap());
248 network_table["network-passphrase"] =
249 value(env_config.network.network_passphrase.as_ref().unwrap());
250 network_table["run-locally"] = value(env_config.network.run_locally);
251 dev_table["network"] = Item::Table(network_table);
252
253 let contracts_table = env_config
255 .contracts
256 .as_ref()
257 .map(|contracts| {
258 contracts
259 .iter()
260 .map(|(name, config)| {
261 let mut contract_constructor_args = Table::new();
262 if let Some(args) = &config.constructor_args {
263 contract_constructor_args["constructor_args"] = value(args);
264 }
265 let contract_key = name.replace('-', "_");
267 (contract_key, Item::Table(contract_constructor_args))
268 })
269 .collect::<Table>()
270 })
271 .unwrap_or_default();
272
273 dev_table["contracts"] = Item::Table(contracts_table);
274
275 doc["development"] = Item::Table(dev_table);
276
277 write(&env_path, doc.to_string())?;
278
279 Ok(())
280 }
281
282 fn discover_contracts(&self, global_args: &Args) -> Result<Vec<String>, Error> {
283 let contracts_dir = self.workspace_path.join("contracts");
284 let printer = Print::new(global_args.quiet);
285
286 let contracts = std::fs::read_dir(&contracts_dir)?
287 .map(|entry_res| -> Result<Option<String>, Error> {
288 let entry = entry_res?;
289 let path = entry.path();
290
291 let cargo_toml = path.join("Cargo.toml");
293 if !path.is_dir() || !cargo_toml.exists() {
294 return Ok(None);
295 }
296
297 let mut content = fs::read_to_string(&cargo_toml)?;
298 if !content.contains("cdylib") {
299 return Ok(None);
300 }
301
302 let tv = content.parse::<toml::Value>()?;
304 let name = tv
305 .get("package")
306 .and_then(|p| p.get("name"))
307 .and_then(|n| n.as_str())
308 .ok_or_else(|| Error::InvalidPackageName)?;
309
310 if content.contains("[package.metadata.stellar]") {
312 printer.infoln("Found metadata section [package.metadata.stellar]");
313 } else {
314 content.push_str("\n[package.metadata.stellar]\ncargo_inherit = true\n");
315
316 let res = write(path.join("Cargo.toml"), content);
317 if let Err(e) = res {
318 printer.errorln(format!("Failed to write Cargo.toml file {e}"));
319 }
320 }
321
322 Ok(Some(name.to_string()))
323 })
324 .collect::<Result<Vec<Option<String>>, Error>>()? .into_iter()
326 .flatten()
327 .collect();
328
329 Ok(contracts)
330 }
331
332 async fn build_contracts(
333 &self,
334 global_args: &stellar_cli::commands::global::Args,
335 ) -> Result<(), Error> {
336 let build_cmd = build::Command {
338 build_clients_args: build::clients::Args {
339 env: Some(build::clients::ScaffoldEnv::Development),
340 workspace_root: Some(self.workspace_path.clone()),
341 out_dir: None,
342 global_args: Some(global_args.clone()),
343 extensions: vec![],
344 compile_ctx: None,
345 },
346 build: stellar_cli::commands::contract::build::Cmd {
347 manifest_path: None,
348 package: None,
349 profile: "release".to_string(),
350 features: None,
351 all_features: false,
352 no_default_features: false,
353 out_dir: None,
354 print_commands_only: false,
355 meta: Vec::new(),
356 optimize: false,
357 },
358 list: false,
359 build_clients: false, };
361
362 build_cmd.run(global_args).await?;
363 Ok(())
364 }
365
366 fn get_constructor_args(&self, contract_name: &str) -> Result<Option<String>, Error> {
367 let target_dir = self.workspace_path.join("target");
369 let wasm_path = stellar_build::stellar_wasm_out_file(&target_dir, contract_name);
370
371 if !wasm_path.exists() {
372 return Err(Error::WasmFileNotFound(contract_name.to_string()));
373 }
374
375 let raw_wasm = fs::read(&wasm_path)?;
377 ArgParser::get_constructor_args(self.skip_prompt, contract_name, &raw_wasm)
378 .map_err(Error::ConstructorArgsError)
379 }
380
381 fn setup_env_file(&self) -> Result<(), Error> {
382 let env_example_path = self.workspace_path.join(".env.example");
383 let env_path = self.workspace_path.join(".env");
384
385 if env_example_path.exists() && !env_path.exists() {
387 let copy_options = fs_extra::file::CopyOptions::new();
388 fs_extra::file::copy(&env_example_path, &env_path, ©_options)?;
389 }
390
391 Ok(())
392 }
393}