Skip to content

Commit ebe9348

Browse files
ddoktorskicptartur
andauthored
Add snforge new command (#2770)
<!-- Reference any GitHub issues resolved by this PR --> Closes #2645 ## Introduced changes <!-- A brief description of the changes --> - `snforge new` command, which allows specifying the name of the package and the path where it should be created - Deprecation warning to the `snforge init` command Docs and changelog will be updated in a separate PR. ## Checklist <!-- Make sure all of these are complete --> - [X] Linked relevant issue - [X] Updated relevant documentation - [X] Added relevant tests - [X] Performed self-review of the code - [X] Added changes to `CHANGELOG.md` --------- Co-authored-by: Artur Michałek <[email protected]>
1 parent 7e4ba6b commit ebe9348

File tree

10 files changed

+420
-236
lines changed

10 files changed

+420
-236
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
- Requirements validation during `snforge` runtime
1515
- `snforge check-requirements` command
16+
- `snforge new` command
1617

1718
#### Changed
1819

@@ -24,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2425

2526
- `--fee-token` and `--version` flags are now optional, `strk` and `v3` will be used by default
2627

28+
#### Deprecated
29+
30+
- `snforge init` command has been deprecated
31+
2732
## [0.34.0] - 2024-11-26
2833

2934
### Forge

crates/forge/src/init.rs

Lines changed: 20 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -1,223 +1,25 @@
1-
use crate::scarb::config::SCARB_MANIFEST_TEMPLATE_CONTENT;
2-
use crate::CAIRO_EDITION;
3-
use anyhow::{anyhow, bail, Context, Ok, Result};
4-
use include_dir::{include_dir, Dir};
5-
use indoc::formatdoc;
6-
use scarb_api::ScarbCommand;
7-
use semver::Version;
8-
use shared::consts::FREE_RPC_PROVIDER_URL;
9-
use std::env;
10-
use std::fs::{self, OpenOptions};
11-
use std::io::Write;
12-
use std::path::{Path, PathBuf};
13-
use toml_edit::{value, ArrayOfTables, DocumentMut, Item, Table};
1+
use crate::{new, NewArgs};
2+
use anyhow::{anyhow, Context, Result};
3+
use camino::Utf8PathBuf;
4+
use shared::print::print_as_warning;
145

15-
static TEMPLATE: Dir = include_dir!("starknet_forge_template");
16-
17-
const DEFAULT_ASSERT_MACROS: Version = Version::new(0, 1, 0);
18-
const MINIMAL_SCARB_FOR_CORRESPONDING_ASSERT_MACROS: Version = Version::new(2, 8, 0);
19-
20-
fn create_snfoundry_manifest(path: &PathBuf) -> Result<()> {
21-
fs::write(
22-
path,
23-
formatdoc! {r#"
24-
# Visit https://foundry-rs.github.io/starknet-foundry/appendix/snfoundry-toml.html
25-
# and https://foundry-rs.github.io/starknet-foundry/projects/configuration.html for more information
26-
27-
# [sncast.default] # Define a profile name
28-
# url = "{default_rpc_url}" # Url of the RPC provider
29-
# accounts-file = "../account-file" # Path to the file with the account data
30-
# account = "mainuser" # Account from `accounts_file` or default account file that will be used for the transactions
31-
# keystore = "~/keystore" # Path to the keystore file
32-
# wait-params = {{ timeout = 300, retry-interval = 10 }} # Wait for submitted transaction parameters
33-
# block-explorer = "StarkScan" # Block explorer service used to display links to transaction details
34-
# show-explorer-links = true # Print links pointing to pages with transaction details in the chosen block explorer
35-
"#,
36-
default_rpc_url = FREE_RPC_PROVIDER_URL,
37-
},
38-
)?;
39-
40-
Ok(())
41-
}
42-
43-
fn add_template_to_scarb_manifest(path: &PathBuf) -> Result<()> {
44-
if !path.exists() {
45-
bail!("Scarb.toml not found");
46-
}
47-
48-
let mut file = OpenOptions::new()
49-
.append(true)
50-
.open(path)
51-
.context("Failed to open Scarb.toml")?;
52-
53-
file.write_all(SCARB_MANIFEST_TEMPLATE_CONTENT.as_bytes())
54-
.context("Failed to write to Scarb.toml")?;
55-
Ok(())
56-
}
57-
58-
fn overwrite_files_from_scarb_template(
59-
dir_to_overwrite: &str,
60-
base_path: &Path,
61-
project_name: &str,
62-
) -> Result<()> {
63-
let copy_from_dir = TEMPLATE.get_dir(dir_to_overwrite).ok_or_else(|| {
64-
anyhow!(
65-
"Directory {} doesn't exist in the template.",
66-
dir_to_overwrite
67-
)
68-
})?;
69-
70-
for file in copy_from_dir.files() {
71-
fs::create_dir_all(base_path.join(Path::new(dir_to_overwrite)))?;
72-
let path = base_path.join(file.path());
73-
let contents = file.contents();
74-
let contents = replace_project_name(contents, project_name)?;
75-
76-
fs::write(path, contents)?;
77-
}
78-
79-
Ok(())
80-
}
81-
82-
fn replace_project_name(contents: &[u8], project_name: &str) -> Result<Vec<u8>> {
83-
let contents = std::str::from_utf8(contents).context("UTF-8 error")?;
84-
let contents = contents.replace("{{ PROJECT_NAME }}", project_name);
85-
Ok(contents.into_bytes())
86-
}
87-
88-
fn update_config(config_path: &Path, scarb: &Version) -> Result<()> {
89-
let config_file = fs::read_to_string(config_path)?;
90-
let mut document = config_file
91-
.parse::<DocumentMut>()
92-
.context("invalid document")?;
93-
94-
add_target_to_toml(&mut document);
95-
set_cairo_edition(&mut document, CAIRO_EDITION);
96-
add_test_script(&mut document);
97-
add_assert_macros(&mut document, scarb)?;
98-
99-
fs::write(config_path, document.to_string())?;
100-
101-
Ok(())
102-
}
103-
104-
fn add_test_script(document: &mut DocumentMut) {
105-
let mut test = Table::new();
106-
107-
test.insert("test", value("snforge test"));
108-
document.insert("scripts", Item::Table(test));
109-
}
110-
111-
fn add_target_to_toml(document: &mut DocumentMut) {
112-
let mut array_of_tables = ArrayOfTables::new();
113-
let mut sierra = Table::new();
114-
let mut contract = Table::new();
115-
contract.set_implicit(true);
116-
117-
sierra.insert("sierra", Item::Value(true.into()));
118-
array_of_tables.push(sierra);
119-
contract.insert("starknet-contract", Item::ArrayOfTables(array_of_tables));
120-
121-
document.insert("target", Item::Table(contract));
122-
}
123-
124-
fn set_cairo_edition(document: &mut DocumentMut, cairo_edition: &str) {
125-
document["package"]["edition"] = value(cairo_edition);
126-
}
127-
128-
fn add_assert_macros(document: &mut DocumentMut, scarb: &Version) -> Result<()> {
129-
let version = if scarb < &MINIMAL_SCARB_FOR_CORRESPONDING_ASSERT_MACROS {
130-
&DEFAULT_ASSERT_MACROS
131-
} else {
132-
scarb
133-
};
134-
135-
document
136-
.get_mut("dev-dependencies")
137-
.and_then(|dep| dep.as_table_mut())
138-
.context("Failed to get dev-dependencies from Scarb.toml")?
139-
.insert("assert_macros", value(version.to_string()));
140-
141-
Ok(())
142-
}
143-
144-
fn extend_gitignore(path: &Path) -> Result<()> {
145-
let mut file = OpenOptions::new()
146-
.append(true)
147-
.open(path.join(".gitignore"))?;
148-
writeln!(file, ".snfoundry_cache/")?;
149-
150-
Ok(())
151-
}
152-
153-
pub fn run(project_name: &str) -> Result<()> {
6+
pub fn init(project_name: &str) -> Result<()> {
1547
let current_dir = std::env::current_dir().context("Failed to get current directory")?;
155-
let project_path = current_dir.join(project_name);
156-
let scarb_manifest_path = project_path.join("Scarb.toml");
157-
let snfoundry_manifest_path = project_path.join("snfoundry.toml");
158-
159-
// if there is no Scarb.toml run `scarb new`
160-
if !scarb_manifest_path.is_file() {
161-
ScarbCommand::new_with_stdio()
162-
.current_dir(current_dir)
163-
.arg("new")
164-
.arg(&project_path)
165-
.env("SCARB_INIT_TEST_RUNNER", "cairo-test")
166-
.run()
167-
.context("Failed to initialize a new project")?;
168-
169-
ScarbCommand::new_with_stdio()
170-
.current_dir(&project_path)
171-
.manifest_path(scarb_manifest_path.clone())
172-
.offline()
173-
.arg("remove")
174-
.arg("--dev")
175-
.arg("cairo_test")
176-
.run()
177-
.context("Failed to remove cairo_test")?;
178-
}
179-
180-
add_template_to_scarb_manifest(&scarb_manifest_path)?;
181-
182-
if !snfoundry_manifest_path.is_file() {
183-
create_snfoundry_manifest(&snfoundry_manifest_path)?;
8+
let project_path = Utf8PathBuf::from_path_buf(current_dir)
9+
.expect("Failed to create Utf8PathBuf for the current directory")
10+
.join(project_name);
11+
12+
// To prevent printing this warning when running scarb init/new with an older version of Scarb
13+
if !project_path.join("Scarb.toml").exists() {
14+
print_as_warning(&anyhow!(
15+
"Command `snforge init` is deprecated and will be removed in the future. Please use `snforge new` instead."
16+
));
18417
}
18518

186-
let version = env!("CARGO_PKG_VERSION");
187-
let cairo_version = ScarbCommand::version().run()?.cairo;
188-
189-
if env::var("DEV_DISABLE_SNFORGE_STD_DEPENDENCY").is_err() {
190-
ScarbCommand::new_with_stdio()
191-
.current_dir(&project_path)
192-
.manifest_path(scarb_manifest_path.clone())
193-
.offline()
194-
.arg("add")
195-
.arg("--dev")
196-
.arg(format!("snforge_std@{version}"))
197-
.run()
198-
.context("Failed to add snforge_std")?;
199-
}
200-
201-
ScarbCommand::new_with_stdio()
202-
.current_dir(&project_path)
203-
.manifest_path(scarb_manifest_path.clone())
204-
.offline()
205-
.arg("add")
206-
.arg(format!("starknet@{cairo_version}"))
207-
.run()
208-
.context("Failed to add starknet")?;
209-
210-
update_config(&project_path.join("Scarb.toml"), &cairo_version)?;
211-
extend_gitignore(&project_path)?;
212-
overwrite_files_from_scarb_template("src", &project_path, project_name)?;
213-
overwrite_files_from_scarb_template("tests", &project_path, project_name)?;
214-
215-
// Fetch to create lock file.
216-
ScarbCommand::new_with_stdio()
217-
.manifest_path(scarb_manifest_path)
218-
.arg("fetch")
219-
.run()
220-
.context("Failed to fetch created project")?;
221-
222-
Ok(())
19+
new::new(NewArgs {
20+
path: project_path,
21+
name: Some(project_name.to_string()),
22+
no_vcs: false,
23+
overwrite: true,
24+
})
22325
}

crates/forge/src/lib.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::compatibility_check::{create_version_parser, Requirement, RequirementsChecker};
22
use anyhow::Result;
3+
use camino::Utf8PathBuf;
34
use clap::{Parser, Subcommand, ValueEnum};
45
use forge_runner::CACHE_DIR;
56
use run_tests::workspace::run_for_workspace;
@@ -14,6 +15,7 @@ pub mod block_number_map;
1415
mod combine_configs;
1516
mod compatibility_check;
1617
mod init;
18+
mod new;
1719
pub mod pretty_printing;
1820
pub mod run_tests;
1921
pub mod scarb;
@@ -76,6 +78,11 @@ enum ForgeSubcommand {
7678
/// Name of a new project
7779
name: String,
7880
},
81+
/// Create a new Forge project at <PATH>
82+
New {
83+
#[command(flatten)]
84+
args: NewArgs,
85+
},
7986
/// Clean Forge cache directory
8087
CleanCache {},
8188
/// Check if all `snforge` requirements are installed
@@ -160,6 +167,21 @@ pub struct TestArgs {
160167
additional_args: Vec<OsString>,
161168
}
162169

170+
#[derive(Parser, Debug)]
171+
pub struct NewArgs {
172+
/// Path to a location where the new project will be created
173+
path: Utf8PathBuf,
174+
/// Name of a new project, defaults to the directory name
175+
#[arg(short, long)]
176+
name: Option<String>,
177+
/// Do not initialize a new Git repository
178+
#[arg(long)]
179+
no_vcs: bool,
180+
/// Try to create the project even if the specified directory at <PATH> is not empty, which can result in overwriting existing files
181+
#[arg(long)]
182+
overwrite: bool,
183+
}
184+
163185
pub enum ExitStatus {
164186
Success,
165187
Failure,
@@ -172,7 +194,11 @@ pub fn main_execution() -> Result<ExitStatus> {
172194

173195
match cli.subcommand {
174196
ForgeSubcommand::Init { name } => {
175-
init::run(name.as_str())?;
197+
init::init(name.as_str())?;
198+
Ok(ExitStatus::Success)
199+
}
200+
ForgeSubcommand::New { args } => {
201+
new::new(args)?;
176202
Ok(ExitStatus::Success)
177203
}
178204
ForgeSubcommand::CleanCache {} => {

0 commit comments

Comments
 (0)