forge/cmd/
init.rs
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 #[command(flatten)]
41 pub install: DependencyInstallOpts,
42}
43
44impl InitArgs {
45 pub fn run(self) -> Result<()> {
46 let Self { root, template, branch, install, offline, force, vscode } = self;
47 let DependencyInstallOpts { shallow, no_git, commit } = install;
48
49 if !root.exists() {
51 fs::create_dir_all(&root)?;
52 }
53 let root = dunce::canonicalize(root)?;
54 let git = Git::new(&root).shallow(shallow);
55
56 if let Some(template) = template {
60 let template = if template.contains("://") {
61 template
62 } else if template.starts_with("github.com/") {
63 "https://".to_string() + &template
64 } else {
65 "https://github.com/".to_string() + &template
66 };
67 sh_println!("Initializing {} from {}...", root.display(), template)?;
68 git.init()?;
70
71 git.fetch(true, &template, branch)?;
74
75 let commit_hash = git.commit_hash(true, "FETCH_HEAD")?;
78 let commit_msg = format!("chore: init from {template} at {commit_hash}");
80 let new_commit_hash = git.commit_tree("FETCH_HEAD^{tree}", Some(commit_msg))?;
82 git.reset(true, new_commit_hash)?;
84
85 if shallow {
87 git.submodule_init()?;
88 } else {
89 git.submodule_update(false, false, true, true, std::iter::empty::<PathBuf>())?;
91 }
92 } else {
93 if root.read_dir().is_ok_and(|mut i| i.next().is_some()) {
95 if !force {
96 eyre::bail!(
97 "Cannot run `init` on a non-empty directory.\n\
98 Run with the `--force` flag to initialize regardless."
99 );
100 }
101 sh_warn!("Target directory is not empty, but `--force` was specified")?;
102 }
103
104 if !no_git && commit && !force && git.is_in_repo()? {
106 git.ensure_clean()?;
107 }
108
109 sh_println!("Initializing {}...", root.display())?;
110
111 let src = root.join("src");
113 fs::create_dir_all(&src)?;
114
115 let test = root.join("test");
116 fs::create_dir_all(&test)?;
117
118 let script = root.join("script");
119 fs::create_dir_all(&script)?;
120
121 let contract_path = src.join("Counter.sol");
123 fs::write(contract_path, include_str!("../../assets/CounterTemplate.sol"))?;
124 let contract_path = test.join("Counter.t.sol");
126 fs::write(contract_path, include_str!("../../assets/CounterTemplate.t.sol"))?;
127 let contract_path = script.join("Counter.s.sol");
129 fs::write(contract_path, include_str!("../../assets/CounterTemplate.s.sol"))?;
130 let readme_path = root.join("README.md");
132 fs::write(readme_path, include_str!("../../assets/README.md"))?;
133
134 let dest = root.join(Config::FILE_NAME);
136 let mut config = Config::load_with_root(&root)?;
137 if !dest.exists() {
138 fs::write(dest, config.clone().into_basic().to_string_pretty()?)?;
139 }
140 let git = self.install.git(&config);
141
142 if !no_git {
144 init_git_repo(git, commit)?;
145 }
146
147 if !offline {
149 if root.join("lib/forge-std").exists() {
150 sh_warn!("\"lib/forge-std\" already exists, skipping install...")?;
151 self.install.install(&mut config, vec![])?;
152 } else {
153 let dep = "https://github.com/foundry-rs/forge-std".parse()?;
154 self.install.install(&mut config, vec![dep])?;
155 }
156 }
157
158 if vscode {
160 init_vscode(&root)?;
161 }
162 }
163
164 sh_println!("{}", " Initialized forge project".green())?;
165 Ok(())
166 }
167}
168
169fn init_git_repo(git: Git<'_>, commit: bool) -> Result<()> {
175 if !git.is_in_repo()? {
177 git.init()?;
178 }
179
180 let gitignore = git.root.join(".gitignore");
182 if !gitignore.exists() {
183 fs::write(gitignore, include_str!("../../assets/.gitignoreTemplate"))?;
184 }
185
186 let workflow = git.root.join(".github/workflows/test.yml");
188 if !workflow.exists() {
189 fs::create_dir_all(workflow.parent().unwrap())?;
190 fs::write(workflow, include_str!("../../assets/workflowTemplate.yml"))?;
191 }
192
193 if commit {
195 git.add(Some("--all"))?;
196 git.commit("chore: forge init")?;
197 }
198
199 Ok(())
200}
201
202fn init_vscode(root: &Path) -> Result<()> {
204 let remappings_file = root.join("remappings.txt");
205 if !remappings_file.exists() {
206 let mut remappings = Remapping::find_many(&root.join("lib"))
207 .into_iter()
208 .map(|r| r.into_relative(root).to_relative_remapping().to_string())
209 .collect::<Vec<_>>();
210 if !remappings.is_empty() {
211 remappings.sort();
212 let content = remappings.join("\n");
213 fs::write(remappings_file, content)?;
214 }
215 }
216
217 let vscode_dir = root.join(".vscode");
218 let settings_file = vscode_dir.join("settings.json");
219 let mut settings = if !vscode_dir.is_dir() {
220 fs::create_dir_all(&vscode_dir)?;
221 serde_json::json!({})
222 } else if settings_file.exists() {
223 foundry_compilers::utils::read_json_file(&settings_file)?
224 } else {
225 serde_json::json!({})
226 };
227
228 let obj = settings.as_object_mut().expect("Expected settings object");
229 let src_key = "solidity.packageDefaultDependenciesContractsDirectory";
231 if !obj.contains_key(src_key) {
232 obj.insert(src_key.to_string(), serde_json::Value::String("src".to_string()));
233 }
234 let lib_key = "solidity.packageDefaultDependenciesDirectory";
235 if !obj.contains_key(lib_key) {
236 obj.insert(lib_key.to_string(), serde_json::Value::String("lib".to_string()));
237 }
238
239 let content = serde_json::to_string_pretty(&settings)?;
240 fs::write(settings_file, content)?;
241
242 Ok(())
243}