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