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 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, conflicts_with = "template")]
47 pub use_parent_git: bool,
48
49 #[arg(long, conflicts_with = "template")]
51 pub empty: bool,
52
53 #[command(flatten)]
54 pub install: DependencyInstallOpts,
55}
56
57impl InitArgs {
58 pub fn run(self) -> Result<()> {
59 let Self {
60 root,
61 template,
62 branch,
63 install,
64 offline,
65 force,
66 vscode,
67 use_parent_git,
68 vyper,
69 empty,
70 } = self;
71 let DependencyInstallOpts { shallow, no_git, commit } = install;
72
73 if !root.exists() {
75 fs::create_dir_all(&root)?;
76 }
77 let root = dunce::canonicalize(root)?;
78 let git = Git::new(&root).shallow(shallow);
79
80 if let Some(template) = template {
84 let template = if template.contains("://") {
85 template
86 } else if template.starts_with("github.com/") {
87 "https://".to_string() + &template
88 } else {
89 "https://github.com/".to_string() + &template
90 };
91 sh_println!("Initializing {} from {}...", root.display(), template)?;
92 git.init()?;
94
95 git.fetch(true, &template, branch)?;
98
99 let commit_hash = git.commit_hash(true, "FETCH_HEAD")?;
102 let commit_msg = format!("chore: init from {template} at {commit_hash}");
104 let new_commit_hash = git.commit_tree("FETCH_HEAD^{tree}", Some(commit_msg))?;
106 git.reset(true, new_commit_hash)?;
108
109 if shallow {
111 git.submodule_init()?;
112 } else {
113 git.submodule_update(false, false, true, true, std::iter::empty::<PathBuf>())?;
115 }
116 } else {
117 if root.read_dir().is_ok_and(|mut i| i.next().is_some()) {
119 if !force {
120 eyre::bail!(
121 "Cannot run `init` on a non-empty directory.\n\
122 Run with the `--force` flag to initialize regardless."
123 );
124 }
125 sh_warn!("Target directory is not empty, but `--force` was specified")?;
126 }
127
128 if !no_git && commit && !force && git.is_in_repo()? {
130 git.ensure_clean()?;
131 }
132
133 sh_println!("Initializing {}...", root.display())?;
134
135 let src = root.join("src");
137 fs::create_dir_all(&src)?;
138
139 let test = root.join("test");
140 fs::create_dir_all(&test)?;
141
142 let script = root.join("script");
143 fs::create_dir_all(&script)?;
144
145 if !empty {
147 if vyper {
148 let contract_path = src.join("Counter.vy");
150 fs::write(
151 contract_path,
152 include_str!("../../assets/vyper/CounterTemplate.vy"),
153 )?;
154 let interface_path = src.join("ICounter.sol");
155 fs::write(
156 interface_path,
157 include_str!("../../assets/vyper/ICounterTemplate.sol"),
158 )?;
159
160 let contract_path = test.join("Counter.t.sol");
162 fs::write(
163 contract_path,
164 include_str!("../../assets/vyper/CounterTemplate.t.sol"),
165 )?;
166
167 let contract_path = script.join("Counter.s.sol");
169 fs::write(
170 contract_path,
171 include_str!("../../assets/vyper/CounterTemplate.s.sol"),
172 )?;
173 } else {
174 let contract_path = src.join("Counter.sol");
176 fs::write(
177 contract_path,
178 include_str!("../../assets/solidity/CounterTemplate.sol"),
179 )?;
180
181 let contract_path = test.join("Counter.t.sol");
183 fs::write(
184 contract_path,
185 include_str!("../../assets/solidity/CounterTemplate.t.sol"),
186 )?;
187
188 let contract_path = script.join("Counter.s.sol");
190 fs::write(
191 contract_path,
192 include_str!("../../assets/solidity/CounterTemplate.s.sol"),
193 )?;
194 }
195 }
196
197 let readme_path = root.join("README.md");
199 fs::write(readme_path, include_str!("../../assets/README.md"))?;
200
201 let dest = root.join(Config::FILE_NAME);
203 let mut config = Config::load_with_root(&root)?;
204 if !dest.exists() {
205 fs::write(dest, config.clone().into_basic().to_string_pretty()?)?;
206 }
207 let git = self.install.git(&config);
208
209 if !no_git {
211 init_git_repo(git, commit, use_parent_git, vyper)?;
212 }
213
214 if !offline {
216 if root.join("lib/forge-std").exists() {
217 sh_warn!("\"lib/forge-std\" already exists, skipping install...")?;
218 self.install.install(&mut config, vec![])?;
219 } else {
220 let dep = "https://github.com/foundry-rs/forge-std".parse()?;
221 self.install.install(&mut config, vec![dep])?;
222 }
223 }
224
225 if vscode {
227 init_vscode(&root)?;
228 }
229 }
230
231 sh_println!("{}", " Initialized forge project".green())?;
232 Ok(())
233 }
234}
235
236fn init_git_repo(git: Git<'_>, commit: bool, use_parent_git: bool, vyper: bool) -> Result<()> {
243 if !git.is_in_repo()? || (!use_parent_git && !git.is_repo_root()?) {
245 git.init()?;
246 }
247
248 let gitignore = git.root.join(".gitignore");
250 if !gitignore.exists() {
251 fs::write(gitignore, include_str!("../../assets/.gitignoreTemplate"))?;
252 }
253
254 let workflow = git.root.join(".github/workflows/test.yml");
256 if !workflow.exists() {
257 fs::create_dir_all(workflow.parent().unwrap())?;
258
259 if vyper {
260 fs::write(workflow, include_str!("../../assets/vyper/workflowTemplate.yml"))?;
261 } else {
262 fs::write(workflow, include_str!("../../assets/solidity/workflowTemplate.yml"))?;
263 }
264 }
265
266 if commit {
268 git.add(Some("--all"))?;
269 git.commit("chore: forge init")?;
270 }
271
272 Ok(())
273}
274
275fn init_vscode(root: &Path) -> Result<()> {
277 let remappings_file = root.join("remappings.txt");
278 if !remappings_file.exists() {
279 let mut remappings = Remapping::find_many(&root.join("lib"))
280 .into_iter()
281 .map(|r| r.into_relative(root).to_relative_remapping().to_string())
282 .collect::<Vec<_>>();
283 if !remappings.is_empty() {
284 remappings.sort();
285 let content = remappings.join("\n");
286 fs::write(remappings_file, content)?;
287 }
288 }
289
290 let vscode_dir = root.join(".vscode");
291 let settings_file = vscode_dir.join("settings.json");
292 let mut settings = if !vscode_dir.is_dir() {
293 fs::create_dir_all(&vscode_dir)?;
294 serde_json::json!({})
295 } else if settings_file.exists() {
296 foundry_compilers::utils::read_json_file(&settings_file)?
297 } else {
298 serde_json::json!({})
299 };
300
301 let obj = settings.as_object_mut().expect("Expected settings object");
302 let src_key = "solidity.packageDefaultDependenciesContractsDirectory";
304 if !obj.contains_key(src_key) {
305 obj.insert(src_key.to_string(), serde_json::Value::String("src".to_string()));
306 }
307 let lib_key = "solidity.packageDefaultDependenciesDirectory";
308 if !obj.contains_key(lib_key) {
309 obj.insert(lib_key.to_string(), serde_json::Value::String("lib".to_string()));
310 }
311
312 let content = serde_json::to_string_pretty(&settings)?;
313 fs::write(settings_file, content)?;
314
315 Ok(())
316}