Skip to main content

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 foundry_evm_networks::{NetworkConfigs, NetworkVariant};
9use std::path::{Path, PathBuf};
10use yansi::Paint;
11
12/// CLI arguments for `forge init`.
13#[derive(Clone, Debug, Default, Parser)]
14pub struct InitArgs {
15    /// The root directory of the new project.
16    #[arg(value_hint = ValueHint::DirPath, default_value = ".", value_name = "PATH")]
17    pub root: PathBuf,
18
19    /// The template to start from.
20    #[arg(long, short)]
21    pub template: Option<String>,
22
23    /// Branch argument that can only be used with template option.
24    /// If not specified, the default branch is used.
25    #[arg(long, short, requires = "template")]
26    pub branch: Option<String>,
27
28    /// Do not install dependencies from the network.
29    #[arg(long, conflicts_with = "template", visible_alias = "no-deps")]
30    pub offline: bool,
31
32    /// Create the project even if the specified root directory is not empty.
33    #[arg(long, conflicts_with = "template")]
34    pub force: bool,
35
36    /// Create a .vscode/settings.json file with Solidity settings, and generate a remappings.txt
37    /// file.
38    #[arg(long, conflicts_with = "template")]
39    pub vscode: bool,
40
41    /// Initialize a Vyper project template.
42    #[arg(long, conflicts_with = "template")]
43    pub vyper: bool,
44
45    /// Initialize a project template for the specified network in Foundry.
46    #[arg(long, short, num_args = 1, value_name = "NETWORK", conflicts_with_all = &["vyper", "template"])]
47    pub network: Option<NetworkVariant>,
48
49    /// Use the parent git repository instead of initializing a new one.
50    /// Only valid if the target is in a git repository.
51    #[arg(long, conflicts_with = "template")]
52    pub use_parent_git: bool,
53
54    /// Do not create example contracts (Counter.sol, Counter.t.sol, Counter.s.sol).
55    #[arg(long, conflicts_with = "template")]
56    pub empty: bool,
57
58    /// Do not create an initial commit.
59    ///
60    /// This is a noop flag kept for backwards compatibility, as `forge init` no longer commits by
61    /// default. Use `--commit` to opt into creating a commit.
62    #[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        // create the root dir if it does not exist
90        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 a template is provided, then this command initializes a git repo,
97        // fetches the template repo, and resets the git history to the head of the fetched
98        // repo with no other history
99        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            // initialize the git repository
109            git.init()?;
110
111            // fetch the template - always fetch shallow for templates since git history will be
112            // collapsed. gitmodules will be initialized after the template is fetched
113            git.fetch(true, &template, branch)?;
114
115            // reset git history to the head of the template
116            // first get the commit hash that was fetched
117            let commit_hash = git.commit_hash(true, "FETCH_HEAD")?;
118            // format a commit message for the new repo
119            let commit_msg = format!("chore: init from {template} at {commit_hash}");
120            // get the hash of the FETCH_HEAD with the new commit message
121            let new_commit_hash = git.commit_tree("FETCH_HEAD^{tree}", Some(commit_msg))?;
122            // reset head of this repo to be the head of the template repo
123            git.reset(true, new_commit_hash)?;
124
125            // if shallow, just initialize submodules
126            if shallow {
127                git.submodule_init()?;
128            } else {
129                // if not shallow, initialize and clone submodules (without fetching latest)
130                git.submodule_update(false, false, true, true, std::iter::empty::<PathBuf>())?;
131            }
132        } else {
133            // if target is not empty
134            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            // ensure git status is clean before generating anything
145            if !no_git && commit && !force && git.is_in_repo()? {
146                git.ensure_clean()?;
147            }
148
149            sh_println!("Initializing {}...", root.display())?;
150
151            // make the dirs
152            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            // Only create example contracts if not disabled
162            if !empty {
163                if vyper {
164                    // write the contract file
165                    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                    // write the tests
177                    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                    // write the script
184                    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                    // write the contract file
191                    let contract_path = src.join("Mail.sol");
192                    fs::write(contract_path, include_str!("../../assets/tempo/MailTemplate.sol"))?;
193
194                    // write the tests
195                    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                    // write the script
202                    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                    // write the contract file
209                    let contract_path = src.join("Counter.sol");
210                    fs::write(
211                        contract_path,
212                        include_str!("../../assets/solidity/CounterTemplate.sol"),
213                    )?;
214
215                    // write the tests
216                    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                    // write the script
223                    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            // Write the README file
232            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            // write foundry.toml, if it doesn't exist already
240            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            // set up the repo
251            if !no_git {
252                init_git_repo(git, commit, use_parent_git, vyper, tempo)?;
253            }
254
255            // install forge-std
256            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                // install tempo-std
266                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            // init vscode settings
278            if vscode {
279                init_vscode(&root)?;
280            }
281        }
282
283        sh_println!("{}", "    Initialized forge project".green())?;
284        Ok(())
285    }
286}
287
288/// Initialises `root` as a git repository, if it isn't one already, unless 'use_parent_git' is
289/// true.
290///
291/// Creates `.gitignore` and `.github/workflows/test.yml`, if they don't exist already.
292///
293/// Commits everything in `root` if `commit` is true.
294fn init_git_repo(
295    git: Git<'_>,
296    commit: bool,
297    use_parent_git: bool,
298    vyper: bool,
299    tempo: bool,
300) -> Result<()> {
301    // `git init`
302    if !git.is_in_repo()? || (!use_parent_git && !git.is_repo_root()?) {
303        git.init()?;
304    }
305
306    // .gitignore
307    let gitignore = git.root.join(".gitignore");
308    if !gitignore.exists() {
309        fs::write(gitignore, include_str!("../../assets/.gitignoreTemplate"))?;
310    }
311
312    // github workflow
313    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    // commit everything
327    if commit {
328        git.add(Some("--all"))?;
329        git.commit("chore: forge init")?;
330    }
331
332    Ok(())
333}
334
335/// initializes the `.vscode/settings.json` file
336fn 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    // insert [vscode-solidity settings](https://github.com/juanfranblanco/vscode-solidity)
363    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}