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