1use super::install::DependencyInstallOpts;
2use clap::{Parser, ValueHint};
3use eyre::Result;
4use foundry_cli::utils::Git;
5use foundry_common::fs;
6use foundry_compilers::artifacts::remappings::Remapping;
7use foundry_config::Config;
8use foundry_evm_networks::{NetworkConfigs, NetworkVariant};
9use std::path::{Path, PathBuf};
10use yansi::Paint;
11
12#[derive(Clone, Debug, Default, Parser)]
14pub struct InitArgs {
15 #[arg(value_hint = ValueHint::DirPath, default_value = ".", value_name = "PATH")]
17 pub root: PathBuf,
18
19 #[arg(long, short)]
21 pub template: Option<String>,
22
23 #[arg(long, short, requires = "template")]
26 pub branch: Option<String>,
27
28 #[arg(long, conflicts_with = "template", visible_alias = "no-deps")]
30 pub offline: bool,
31
32 #[arg(long, conflicts_with = "template")]
34 pub force: bool,
35
36 #[arg(long, conflicts_with = "template")]
39 pub vscode: bool,
40
41 #[arg(long, conflicts_with = "template")]
43 pub vyper: bool,
44
45 #[arg(long, short, num_args = 1, value_name = "NETWORK", conflicts_with_all = &["vyper", "template"])]
47 pub network: Option<NetworkVariant>,
48
49 #[arg(long, conflicts_with = "template")]
52 pub use_parent_git: bool,
53
54 #[arg(long, conflicts_with = "template")]
56 pub empty: bool,
57
58 #[arg(long, hide = true)]
63 pub no_commit: bool,
64
65 #[command(flatten)]
66 pub install: DependencyInstallOpts,
67}
68
69impl InitArgs {
70 pub async fn run(self) -> Result<()> {
71 let Self {
72 root,
73 template,
74 branch,
75 install,
76 offline,
77 force,
78 vscode,
79 use_parent_git,
80 vyper,
81 network,
82 empty,
83 no_commit: _,
84 } = self;
85 let DependencyInstallOpts { shallow, no_git, commit } = install;
86
87 let tempo = matches!(network, Some(NetworkVariant::Tempo));
88
89 if !root.exists() {
91 fs::create_dir_all(&root)?;
92 }
93 let root = dunce::canonicalize(root)?;
94 let git = Git::new(&root).shallow(shallow);
95
96 if let Some(template) = template {
100 let template = if template.contains("://") {
101 template
102 } else if template.starts_with("github.com/") {
103 "https://".to_string() + &template
104 } else {
105 "https://github.com/".to_string() + &template
106 };
107 sh_println!("Initializing {} from {}...", root.display(), template)?;
108 git.init()?;
110
111 git.fetch(true, &template, branch)?;
114
115 let commit_hash = git.commit_hash(true, "FETCH_HEAD")?;
118 let commit_msg = format!("chore: init from {template} at {commit_hash}");
120 let new_commit_hash = git.commit_tree("FETCH_HEAD^{tree}", Some(commit_msg))?;
122 git.reset(true, new_commit_hash)?;
124
125 if shallow {
127 git.submodule_init()?;
128 } else {
129 git.submodule_update(false, false, true, true, std::iter::empty::<PathBuf>())?;
131 }
132 } else {
133 if root.read_dir().is_ok_and(|mut i| i.next().is_some()) {
135 if !force {
136 eyre::bail!(
137 "Cannot run `init` on a non-empty directory.\n\
138 Run with the `--force` flag to initialize regardless."
139 );
140 }
141 sh_warn!("Target directory is not empty, but `--force` was specified")?;
142 }
143
144 if !no_git && commit && !force && git.is_in_repo()? {
146 git.ensure_clean()?;
147 }
148
149 sh_println!("Initializing {}...", root.display())?;
150
151 let src = root.join("src");
153 fs::create_dir_all(&src)?;
154
155 let test = root.join("test");
156 fs::create_dir_all(&test)?;
157
158 let script = root.join("script");
159 fs::create_dir_all(&script)?;
160
161 if !empty {
163 if vyper {
164 let contract_path = src.join("Counter.vy");
166 fs::write(
167 contract_path,
168 include_str!("../../assets/vyper/CounterTemplate.vy"),
169 )?;
170 let interface_path = src.join("ICounter.sol");
171 fs::write(
172 interface_path,
173 include_str!("../../assets/vyper/ICounterTemplate.sol"),
174 )?;
175
176 let contract_path = test.join("Counter.t.sol");
178 fs::write(
179 contract_path,
180 include_str!("../../assets/vyper/CounterTemplate.t.sol"),
181 )?;
182
183 let contract_path = script.join("Counter.s.sol");
185 fs::write(
186 contract_path,
187 include_str!("../../assets/vyper/CounterTemplate.s.sol"),
188 )?;
189 } else if tempo {
190 let contract_path = src.join("Mail.sol");
192 fs::write(contract_path, include_str!("../../assets/tempo/MailTemplate.sol"))?;
193
194 let contract_path = test.join("Mail.t.sol");
196 fs::write(
197 contract_path,
198 include_str!("../../assets/tempo/MailTemplate.t.sol"),
199 )?;
200
201 let contract_path = script.join("Mail.s.sol");
203 fs::write(
204 contract_path,
205 include_str!("../../assets/tempo/MailTemplate.s.sol"),
206 )?;
207 } else {
208 let contract_path = src.join("Counter.sol");
210 fs::write(
211 contract_path,
212 include_str!("../../assets/solidity/CounterTemplate.sol"),
213 )?;
214
215 let contract_path = test.join("Counter.t.sol");
217 fs::write(
218 contract_path,
219 include_str!("../../assets/solidity/CounterTemplate.t.sol"),
220 )?;
221
222 let contract_path = script.join("Counter.s.sol");
224 fs::write(
225 contract_path,
226 include_str!("../../assets/solidity/CounterTemplate.s.sol"),
227 )?;
228 }
229 }
230
231 let readme_path = root.join("README.md");
233 if tempo {
234 fs::write(readme_path, include_str!("../../assets/tempo/README.md"))?;
235 } else {
236 fs::write(readme_path, include_str!("../../assets/README.md"))?;
237 }
238
239 let dest = root.join(Config::FILE_NAME);
241 let mut config = Config::load_with_root(&root)?;
242 if tempo {
243 config.networks = NetworkConfigs::with_tempo();
244 }
245 if !dest.exists() {
246 fs::write(dest, config.clone().into_basic().to_string_pretty()?)?;
247 }
248 let git = self.install.git(&config);
249
250 if !no_git {
252 init_git_repo(git, commit, use_parent_git, vyper, tempo)?;
253 }
254
255 if !offline {
257 if root.join("lib/forge-std").exists() {
258 sh_warn!("\"lib/forge-std\" already exists, skipping install...")?;
259 self.install.install(&mut config, vec![]).await?;
260 } else {
261 let dep = "https://github.com/foundry-rs/forge-std".parse()?;
262 self.install.install(&mut config, vec![dep]).await?;
263 }
264
265 if tempo {
267 if root.join("lib/tempo-std").exists() {
268 sh_warn!("\"lib/tempo-std\" already exists, skipping install...")?;
269 self.install.install(&mut config, vec![]).await?;
270 } else {
271 let dep = "https://github.com/tempoxyz/tempo-std".parse()?;
272 self.install.install(&mut config, vec![dep]).await?;
273 }
274 }
275 }
276
277 if vscode {
279 init_vscode(&root)?;
280 }
281 }
282
283 sh_println!("{}", " Initialized forge project".green())?;
284 Ok(())
285 }
286}
287
288fn init_git_repo(
295 git: Git<'_>,
296 commit: bool,
297 use_parent_git: bool,
298 vyper: bool,
299 tempo: bool,
300) -> Result<()> {
301 if !git.is_in_repo()? || (!use_parent_git && !git.is_repo_root()?) {
303 git.init()?;
304 }
305
306 let gitignore = git.root.join(".gitignore");
308 if !gitignore.exists() {
309 fs::write(gitignore, include_str!("../../assets/.gitignoreTemplate"))?;
310 }
311
312 let workflow = git.root.join(".github/workflows/test.yml");
314 if !workflow.exists() {
315 fs::create_dir_all(workflow.parent().unwrap())?;
316
317 if vyper {
318 fs::write(workflow, include_str!("../../assets/vyper/workflowTemplate.yml"))?;
319 } else if tempo {
320 fs::write(workflow, include_str!("../../assets/tempo/workflowTemplate.yml"))?;
321 } else {
322 fs::write(workflow, include_str!("../../assets/solidity/workflowTemplate.yml"))?;
323 }
324 }
325
326 if commit {
328 git.add(Some("--all"))?;
329 git.commit("chore: forge init")?;
330 }
331
332 Ok(())
333}
334
335fn init_vscode(root: &Path) -> Result<()> {
337 let remappings_file = root.join("remappings.txt");
338 if !remappings_file.exists() {
339 let mut remappings = Remapping::find_many(&root.join("lib"))
340 .into_iter()
341 .map(|r| r.into_relative(root).to_relative_remapping().to_string())
342 .collect::<Vec<_>>();
343 if !remappings.is_empty() {
344 remappings.sort();
345 let content = remappings.join("\n");
346 fs::write(remappings_file, content)?;
347 }
348 }
349
350 let vscode_dir = root.join(".vscode");
351 let settings_file = vscode_dir.join("settings.json");
352 let mut settings = if !vscode_dir.is_dir() {
353 fs::create_dir_all(&vscode_dir)?;
354 serde_json::json!({})
355 } else if settings_file.exists() {
356 foundry_compilers::utils::read_json_file(&settings_file)?
357 } else {
358 serde_json::json!({})
359 };
360
361 let obj = settings.as_object_mut().expect("Expected settings object");
362 let src_key = "solidity.packageDefaultDependenciesContractsDirectory";
364 if !obj.contains_key(src_key) {
365 obj.insert(src_key.to_string(), serde_json::Value::String("src".to_string()));
366 }
367 let lib_key = "solidity.packageDefaultDependenciesDirectory";
368 if !obj.contains_key(lib_key) {
369 obj.insert(lib_key.to_string(), serde_json::Value::String("lib".to_string()));
370 }
371
372 let content = serde_json::to_string_pretty(&settings)?;
373 fs::write(settings_file, content)?;
374
375 Ok(())
376}