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/// Supported networks for `forge init --network <NETWORK>`
12#[derive(Clone, Debug, clap::ValueEnum)]
13pub enum Networks {
14    Tempo,
15}
16
17/// CLI arguments for `forge init`.
18#[derive(Clone, Debug, Default, Parser)]
19pub struct InitArgs {
20    /// The root directory of the new project.
21    #[arg(value_hint = ValueHint::DirPath, default_value = ".", value_name = "PATH")]
22    pub root: PathBuf,
23
24    /// The template to start from.
25    #[arg(long, short)]
26    pub template: Option<String>,
27
28    /// Branch argument that can only be used with template option.
29    /// If not specified, the default branch is used.
30    #[arg(long, short, requires = "template")]
31    pub branch: Option<String>,
32
33    /// Do not install dependencies from the network.
34    #[arg(long, conflicts_with = "template", visible_alias = "no-deps")]
35    pub offline: bool,
36
37    /// Create the project even if the specified root directory is not empty.
38    #[arg(long, conflicts_with = "template")]
39    pub force: bool,
40
41    /// Create a .vscode/settings.json file with Solidity settings, and generate a remappings.txt
42    /// file.
43    #[arg(long, conflicts_with = "template")]
44    pub vscode: bool,
45
46    /// Initialize a Vyper project template.
47    #[arg(long, conflicts_with = "template")]
48    pub vyper: bool,
49
50    /// Initialize a project template for the specified network in Foundry.
51    #[arg(long, short, conflicts_with_all = &["vyper", "template"])]
52    pub network: Option<Networks>,
53
54    /// Use the parent git repository instead of initializing a new one.
55    /// Only valid if the target is in a git repository.
56    #[arg(long, conflicts_with = "template")]
57    pub use_parent_git: bool,
58
59    /// Do not create example contracts (Counter.sol, Counter.t.sol, Counter.s.sol).
60    #[arg(long, conflicts_with = "template")]
61    pub empty: bool,
62
63    #[command(flatten)]
64    pub install: DependencyInstallOpts,
65}
66
67impl InitArgs {
68    pub async fn run(self) -> Result<()> {
69        let Self {
70            root,
71            template,
72            branch,
73            install,
74            offline,
75            force,
76            vscode,
77            use_parent_git,
78            vyper,
79            network,
80            empty,
81        } = self;
82        let DependencyInstallOpts { shallow, no_git, commit } = install;
83
84        let tempo = matches!(network, Some(Networks::Tempo));
85
86        // create the root dir if it does not exist
87        if !root.exists() {
88            fs::create_dir_all(&root)?;
89        }
90        let root = dunce::canonicalize(root)?;
91        let git = Git::new(&root).shallow(shallow);
92
93        // if a template is provided, then this command initializes a git repo,
94        // fetches the template repo, and resets the git history to the head of the fetched
95        // repo with no other history
96        if let Some(template) = template {
97            let template = if template.contains("://") {
98                template
99            } else if template.starts_with("github.com/") {
100                "https://".to_string() + &template
101            } else {
102                "https://github.com/".to_string() + &template
103            };
104            sh_println!("Initializing {} from {}...", root.display(), template)?;
105            // initialize the git repository
106            git.init()?;
107
108            // fetch the template - always fetch shallow for templates since git history will be
109            // collapsed. gitmodules will be initialized after the template is fetched
110            git.fetch(true, &template, branch)?;
111
112            // reset git history to the head of the template
113            // first get the commit hash that was fetched
114            let commit_hash = git.commit_hash(true, "FETCH_HEAD")?;
115            // format a commit message for the new repo
116            let commit_msg = format!("chore: init from {template} at {commit_hash}");
117            // get the hash of the FETCH_HEAD with the new commit message
118            let new_commit_hash = git.commit_tree("FETCH_HEAD^{tree}", Some(commit_msg))?;
119            // reset head of this repo to be the head of the template repo
120            git.reset(true, new_commit_hash)?;
121
122            // if shallow, just initialize submodules
123            if shallow {
124                git.submodule_init()?;
125            } else {
126                // if not shallow, initialize and clone submodules (without fetching latest)
127                git.submodule_update(false, false, true, true, std::iter::empty::<PathBuf>())?;
128            }
129        } else {
130            // if target is not empty
131            if root.read_dir().is_ok_and(|mut i| i.next().is_some()) {
132                if !force {
133                    eyre::bail!(
134                        "Cannot run `init` on a non-empty directory.\n\
135                        Run with the `--force` flag to initialize regardless."
136                    );
137                }
138                sh_warn!("Target directory is not empty, but `--force` was specified")?;
139            }
140
141            // ensure git status is clean before generating anything
142            if !no_git && commit && !force && git.is_in_repo()? {
143                git.ensure_clean()?;
144            }
145
146            sh_println!("Initializing {}...", root.display())?;
147
148            // make the dirs
149            let src = root.join("src");
150            fs::create_dir_all(&src)?;
151
152            let test = root.join("test");
153            fs::create_dir_all(&test)?;
154
155            let script = root.join("script");
156            fs::create_dir_all(&script)?;
157
158            // Only create example contracts if not disabled
159            if !empty {
160                if vyper {
161                    // write the contract file
162                    let contract_path = src.join("Counter.vy");
163                    fs::write(
164                        contract_path,
165                        include_str!("../../assets/vyper/CounterTemplate.vy"),
166                    )?;
167                    let interface_path = src.join("ICounter.sol");
168                    fs::write(
169                        interface_path,
170                        include_str!("../../assets/vyper/ICounterTemplate.sol"),
171                    )?;
172
173                    // write the tests
174                    let contract_path = test.join("Counter.t.sol");
175                    fs::write(
176                        contract_path,
177                        include_str!("../../assets/vyper/CounterTemplate.t.sol"),
178                    )?;
179
180                    // write the script
181                    let contract_path = script.join("Counter.s.sol");
182                    fs::write(
183                        contract_path,
184                        include_str!("../../assets/vyper/CounterTemplate.s.sol"),
185                    )?;
186                } else if tempo {
187                    // write the contract file
188                    let contract_path = src.join("Mail.sol");
189                    fs::write(contract_path, include_str!("../../assets/tempo/MailTemplate.sol"))?;
190
191                    // write the tests
192                    let contract_path = test.join("Mail.t.sol");
193                    fs::write(
194                        contract_path,
195                        include_str!("../../assets/tempo/MailTemplate.t.sol"),
196                    )?;
197
198                    // write the script
199                    let contract_path = script.join("Mail.s.sol");
200                    fs::write(
201                        contract_path,
202                        include_str!("../../assets/tempo/MailTemplate.s.sol"),
203                    )?;
204                } else {
205                    // write the contract file
206                    let contract_path = src.join("Counter.sol");
207                    fs::write(
208                        contract_path,
209                        include_str!("../../assets/solidity/CounterTemplate.sol"),
210                    )?;
211
212                    // write the tests
213                    let contract_path = test.join("Counter.t.sol");
214                    fs::write(
215                        contract_path,
216                        include_str!("../../assets/solidity/CounterTemplate.t.sol"),
217                    )?;
218
219                    // write the script
220                    let contract_path = script.join("Counter.s.sol");
221                    fs::write(
222                        contract_path,
223                        include_str!("../../assets/solidity/CounterTemplate.s.sol"),
224                    )?;
225                }
226            }
227
228            // Write the README file
229            let readme_path = root.join("README.md");
230            if tempo {
231                fs::write(readme_path, include_str!("../../assets/tempo/README.md"))?;
232            } else {
233                fs::write(readme_path, include_str!("../../assets/README.md"))?;
234            }
235
236            // write foundry.toml, if it doesn't exist already
237            let dest = root.join(Config::FILE_NAME);
238            let mut config = Config::load_with_root(&root)?;
239            if !dest.exists() {
240                fs::write(dest, config.clone().into_basic().to_string_pretty()?)?;
241            }
242            let git = self.install.git(&config);
243
244            // set up the repo
245            if !no_git {
246                init_git_repo(git, commit, use_parent_git, vyper, tempo)?;
247            }
248
249            // install forge-std
250            if !offline {
251                if root.join("lib/forge-std").exists() {
252                    sh_warn!("\"lib/forge-std\" already exists, skipping install...")?;
253                    self.install.install(&mut config, vec![]).await?;
254                } else {
255                    let dep = "https://github.com/foundry-rs/forge-std".parse()?;
256                    self.install.install(&mut config, vec![dep]).await?;
257                }
258
259                // install tempo-std
260                if tempo {
261                    if root.join("lib/tempo-std").exists() {
262                        sh_warn!("\"lib/tempo-std\" already exists, skipping install...")?;
263                        self.install.install(&mut config, vec![]).await?;
264                    } else {
265                        let dep = "https://github.com/tempoxyz/tempo-std".parse()?;
266                        self.install.install(&mut config, vec![dep]).await?;
267                    }
268                }
269            }
270
271            // init vscode settings
272            if vscode {
273                init_vscode(&root)?;
274            }
275        }
276
277        sh_println!("{}", "    Initialized forge project".green())?;
278        Ok(())
279    }
280}
281
282/// Initialises `root` as a git repository, if it isn't one already, unless 'use_parent_git' is
283/// true.
284///
285/// Creates `.gitignore` and `.github/workflows/test.yml`, if they don't exist already.
286///
287/// Commits everything in `root` if `commit` is true.
288fn init_git_repo(
289    git: Git<'_>,
290    commit: bool,
291    use_parent_git: bool,
292    vyper: bool,
293    tempo: bool,
294) -> Result<()> {
295    // `git init`
296    if !git.is_in_repo()? || (!use_parent_git && !git.is_repo_root()?) {
297        git.init()?;
298    }
299
300    // .gitignore
301    let gitignore = git.root.join(".gitignore");
302    if !gitignore.exists() {
303        fs::write(gitignore, include_str!("../../assets/.gitignoreTemplate"))?;
304    }
305
306    // github workflow
307    let workflow = git.root.join(".github/workflows/test.yml");
308    if !workflow.exists() {
309        fs::create_dir_all(workflow.parent().unwrap())?;
310
311        if vyper {
312            fs::write(workflow, include_str!("../../assets/vyper/workflowTemplate.yml"))?;
313        } else if tempo {
314            fs::write(workflow, include_str!("../../assets/tempo/workflowTemplate.yml"))?;
315        } else {
316            fs::write(workflow, include_str!("../../assets/solidity/workflowTemplate.yml"))?;
317        }
318    }
319
320    // commit everything
321    if commit {
322        git.add(Some("--all"))?;
323        git.commit("chore: forge init")?;
324    }
325
326    Ok(())
327}
328
329/// initializes the `.vscode/settings.json` file
330fn init_vscode(root: &Path) -> Result<()> {
331    let remappings_file = root.join("remappings.txt");
332    if !remappings_file.exists() {
333        let mut remappings = Remapping::find_many(&root.join("lib"))
334            .into_iter()
335            .map(|r| r.into_relative(root).to_relative_remapping().to_string())
336            .collect::<Vec<_>>();
337        if !remappings.is_empty() {
338            remappings.sort();
339            let content = remappings.join("\n");
340            fs::write(remappings_file, content)?;
341        }
342    }
343
344    let vscode_dir = root.join(".vscode");
345    let settings_file = vscode_dir.join("settings.json");
346    let mut settings = if !vscode_dir.is_dir() {
347        fs::create_dir_all(&vscode_dir)?;
348        serde_json::json!({})
349    } else if settings_file.exists() {
350        foundry_compilers::utils::read_json_file(&settings_file)?
351    } else {
352        serde_json::json!({})
353    };
354
355    let obj = settings.as_object_mut().expect("Expected settings object");
356    // insert [vscode-solidity settings](https://github.com/juanfranblanco/vscode-solidity)
357    let src_key = "solidity.packageDefaultDependenciesContractsDirectory";
358    if !obj.contains_key(src_key) {
359        obj.insert(src_key.to_string(), serde_json::Value::String("src".to_string()));
360    }
361    let lib_key = "solidity.packageDefaultDependenciesDirectory";
362    if !obj.contains_key(lib_key) {
363        obj.insert(lib_key.to_string(), serde_json::Value::String("lib".to_string()));
364    }
365
366    let content = serde_json::to_string_pretty(&settings)?;
367    fs::write(settings_file, content)?;
368
369    Ok(())
370}