Skip to main content

forge/cmd/
init.rs

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