From 53361ee726dacdb5d1ffa2f4f5354b1476b02f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaya=20G=C3=B6kalp?= Date: Tue, 24 May 2022 02:47:41 +0300 Subject: [PATCH] forc template implementation (#1614) * forc template resolves the HEAD and fetches the repo. Prints the requested directorie's path * forc template works with and without template name * forc init --template removed, pkg.rs unused pub modifier removed * extra info added before copy, cargo-toml-lint, udeps fix, mdbook fix * invoke CI * Update scripts/mdbook-forc-documenter/examples/forc_template.md Co-authored-by: John Adler * Update scripts/mdbook-forc-documenter/examples/forc_template.md Co-authored-by: John Adler * default url provided, project_name is last parameter to pass without --project_name * cargo fmt * println to tracing::info * local_repo_name to template_name Co-authored-by: John Adler --- Cargo.lock | 70 +--- docs/src/SUMMARY.md | 1 + docs/src/forc/commands/forc_template.md | 1 + forc-pkg/src/pkg.rs | 8 +- forc/Cargo.toml | 2 +- forc/src/cli/commands/init.rs | 8 - forc/src/cli/commands/mod.rs | 1 + forc/src/cli/commands/template.rs | 23 ++ forc/src/cli/mod.rs | 5 +- forc/src/ops/forc_init.rs | 337 +----------------- forc/src/ops/forc_template.rs | 172 +++++++++ forc/src/ops/mod.rs | 1 + .../examples/forc_template.md | 16 + 13 files changed, 247 insertions(+), 398 deletions(-) create mode 100644 docs/src/forc/commands/forc_template.md create mode 100644 forc/src/cli/commands/template.rs create mode 100644 forc/src/ops/forc_template.rs create mode 100644 scripts/mdbook-forc-documenter/examples/forc_template.md diff --git a/Cargo.lock b/Cargo.lock index 3f135ebb9ab..c7f603db51d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,12 +12,6 @@ dependencies = [ "regex", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "aead" version = "0.3.2" @@ -523,12 +517,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "chunked_transfer" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" - [[package]] name = "cipher" version = "0.2.5" @@ -691,15 +679,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if 1.0.0", -] - [[package]] name = "crossbeam-queue" version = "0.3.5" @@ -1086,18 +1065,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" -[[package]] -name = "flate2" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" -dependencies = [ - "cfg-if 1.0.0", - "crc32fast", - "libc", - "miniz_oxide", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1114,6 +1081,7 @@ dependencies = [ "clap_complete", "forc-pkg", "forc-util", + "fs_extra", "fuel-asm", "fuel-gql-client", "fuel-tx", @@ -1130,7 +1098,6 @@ dependencies = [ "toml", "toml_edit", "tracing", - "ureq", "url", "uwuify", "walkdir", @@ -1229,6 +1196,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + [[package]] name = "fuel-asm" version = "0.4.0" @@ -2157,16 +2130,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "miniz_oxide" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" -dependencies = [ - "adler", - "autocfg", -] - [[package]] name = "mio" version = "0.8.2" @@ -4166,25 +4129,6 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" -[[package]] -name = "ureq" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9399fa2f927a3d327187cbd201480cee55bee6ac5d3c77dd27f0c6814cff16d5" -dependencies = [ - "base64 0.13.0", - "chunked_transfer", - "flate2", - "log", - "once_cell", - "rustls 0.20.4", - "serde", - "serde_json", - "url", - "webpki 0.22.0", - "webpki-roots 0.22.3", -] - [[package]] name = "url" version = "2.2.2" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 0bbdbc6b503..32d70e55d55 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -51,6 +51,7 @@ - [forc run](./forc/commands/forc_run.md) - [forc test](./forc/commands/forc_test.md) - [forc update](./forc/commands/forc_update.md) + - [forc template](./forc/commands/forc_template.md) - [Plugins](./forc/plugins.md) - [forc explore](./forc_explore.md) - [forc fmt](./forc_fmt.md) diff --git a/docs/src/forc/commands/forc_template.md b/docs/src/forc/commands/forc_template.md new file mode 100644 index 00000000000..a6cd711e7aa --- /dev/null +++ b/docs/src/forc/commands/forc_template.md @@ -0,0 +1 @@ +# forc template diff --git a/forc-pkg/src/pkg.rs b/forc-pkg/src/pkg.rs index 3d01423ea6a..1bd11b90ba6 100644 --- a/forc-pkg/src/pkg.rs +++ b/forc-pkg/src/pkg.rs @@ -619,7 +619,7 @@ pub(crate) fn fetch_deps( /// Produce a unique ID for a particular fetch pass. /// /// This is used in the temporary git directory and allows for avoiding contention over the git repo directory. -fn fetch_id(path: &Path, timestamp: std::time::Instant) -> u64 { +pub fn fetch_id(path: &Path, timestamp: std::time::Instant) -> u64 { let mut hasher = hash_map::DefaultHasher::new(); path.hash(&mut hasher); timestamp.hash(&mut hasher); @@ -781,7 +781,7 @@ where /// /// This clones the repository to a temporary directory in order to determine the commit at the /// HEAD of the given git reference. -fn pin_git(fetch_id: u64, name: &str, source: SourceGit) -> Result { +pub fn pin_git(fetch_id: u64, name: &str, source: SourceGit) -> Result { let commit_hash = with_tmp_git_repo(fetch_id, name, &source, |repo| { // Resolve the reference to the commit ID. let commit_id = source @@ -856,7 +856,7 @@ fn pin_pkg(fetch_id: u64, pkg: &Pkg, path_map: &mut PathMap, sway_git_tag: &str) /// ``` /// /// where `` is a hash of the source repository URL. -fn git_commit_path(name: &str, repo: &Url, commit_hash: &str) -> PathBuf { +pub fn git_commit_path(name: &str, repo: &Url, commit_hash: &str) -> PathBuf { let repo_dir_name = git_repo_dir_name(name, repo); git_checkouts_directory() .join(repo_dir_name) @@ -866,7 +866,7 @@ fn git_commit_path(name: &str, repo: &Url, commit_hash: &str) -> PathBuf { /// Fetch the repo at the given git package's URL and checkout the pinned commit. /// /// Returns the location of the checked out commit. -fn fetch_git(fetch_id: u64, name: &str, pinned: &SourceGitPinned) -> Result { +pub fn fetch_git(fetch_id: u64, name: &str, pinned: &SourceGitPinned) -> Result { let path = git_commit_path(name, &pinned.source.repo, &pinned.commit_hash); // Checkout the pinned hash to the path. diff --git a/forc/Cargo.toml b/forc/Cargo.toml index d53e8557bba..f4a13a55e4d 100644 --- a/forc/Cargo.toml +++ b/forc/Cargo.toml @@ -23,6 +23,7 @@ clap = { version = "3.1", features = ["cargo", "derive", "env"] } clap_complete = "3.1" forc-pkg = { version = "0.13.0", path = "../forc-pkg" } forc-util = { version = "0.13.0", path = "../forc-util" } +fs_extra = "1.2" fuel-asm = "0.4" fuel-gql-client = { version = "0.6", default-features = false } fuel-tx = "0.9" @@ -39,7 +40,6 @@ tokio = { version = "1.8.0", features = ["macros", "rt-multi-thread", "process"] toml = "0.5" toml_edit = "0.13" tracing = "0.1" -ureq = { version = "2.4", features = ["json"] } url = "2.2" uwuify = { version = "^0.2", optional = true } walkdir = "2.3" diff --git a/forc/src/cli/commands/init.rs b/forc/src/cli/commands/init.rs index 37d2f676bab..b3f6e62728a 100644 --- a/forc/src/cli/commands/init.rs +++ b/forc/src/cli/commands/init.rs @@ -2,17 +2,9 @@ use crate::ops::forc_init; use anyhow::Result; use clap::Parser; -const TEMPLATE_HELP: &str = r#"Initialize a new project from a template. - -Example Templates: - - counter"#; - /// Create a new Forc project. #[derive(Debug, Parser)] pub struct Command { - /// Initialize a new project from a template - #[clap(short, long, help = TEMPLATE_HELP)] - pub template: Option, /// The default program type, excluding all flags or adding this flag creates a basic contract program. #[clap(long)] pub contract: bool, diff --git a/forc/src/cli/commands/mod.rs b/forc/src/cli/commands/mod.rs index e1a9bdeaa06..90e3bc394ba 100644 --- a/forc/src/cli/commands/mod.rs +++ b/forc/src/cli/commands/mod.rs @@ -8,5 +8,6 @@ pub mod json_abi; pub mod parse_bytecode; pub mod plugins; pub mod run; +pub mod template; pub mod test; pub mod update; diff --git a/forc/src/cli/commands/template.rs b/forc/src/cli/commands/template.rs new file mode 100644 index 00000000000..12f07a39f2c --- /dev/null +++ b/forc/src/cli/commands/template.rs @@ -0,0 +1,23 @@ +use crate::ops::forc_template; +use anyhow::Result; +use clap::Parser; + +/// Create a new Forc project from a git template. +#[derive(Debug, Parser)] +pub struct Command { + /// The template url, should be a git repo. + #[clap(long, short, default_value = "https://github.com/fuellabs/sway")] + pub url: String, + + /// The name of the template that needs to be fetched and used from git repo provided. + #[clap(long, short)] + pub template_name: Option, + + /// The name of the project that will be created + pub project_name: String, +} + +pub(crate) fn exec(command: Command) -> Result<()> { + forc_template::init(command)?; + Ok(()) +} diff --git a/forc/src/cli/mod.rs b/forc/src/cli/mod.rs index 84eae443992..c4e6139014d 100644 --- a/forc/src/cli/mod.rs +++ b/forc/src/cli/mod.rs @@ -1,6 +1,6 @@ use self::commands::{ addr2line, build, clean, completions, deploy, init, json_abi, parse_bytecode, plugins, run, - test, update, + template, test, update, }; use addr2line::Command as Addr2LineCommand; use anyhow::{anyhow, Result}; @@ -14,6 +14,7 @@ pub use json_abi::Command as JsonAbiCommand; use parse_bytecode::Command as ParseBytecodeCommand; pub use plugins::Command as PluginsCommand; pub use run::Command as RunCommand; +pub use template::Command as TemplateCommand; use test::Command as TestCommand; pub use update::Command as UpdateCommand; @@ -45,6 +46,7 @@ enum Forc { Update(UpdateCommand), JsonAbi(JsonAbiCommand), Plugins(PluginsCommand), + Template(TemplateCommand), /// This is a catch-all for unknown subcommands and their arguments. /// /// When we receive an unknown subcommand, we check for a plugin exe named @@ -72,6 +74,7 @@ pub async fn run_cli() -> Result<()> { Forc::Test(command) => test::exec(command), Forc::Update(command) => update::exec(command).await, Forc::JsonAbi(command) => json_abi::exec(command), + Forc::Template(command) => template::exec(command), Forc::Plugin(args) => { let output = plugin::execute_external_subcommand(args)?; let code = output diff --git a/forc/src/ops/forc_init.rs b/forc/src/ops/forc_init.rs index 322543a8dab..7ed5a24108d 100644 --- a/forc/src/ops/forc_init.rs +++ b/forc/src/ops/forc_init.rs @@ -2,25 +2,14 @@ use crate::cli::InitCommand; use crate::utils::{ defaults, program_type::{ProgramType, ProgramType::*}, - SWAY_GIT_TAG, }; -use anyhow::{Context, Result}; +use anyhow::Result; use forc_util::{println_green, validate_name}; use serde::Deserialize; use std::fs; -use std::fs::File; -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; +use std::path::Path; use sway_utils::constants; use tracing::info; -use url::Url; - -#[derive(Debug)] -struct GitPathInfo { - owner: String, - repo_name: String, - example_name: String, -} #[derive(Debug, Deserialize)] #[serde(rename_all = "lowercase")] @@ -111,89 +100,23 @@ pub fn init(command: InitCommand) -> Result<()> { let project_name = command.project_name; validate_name(&project_name, "project name")?; - match command.template { - Some(template) => { - let example_url = - format!("https://github.com/FuelLabs/sway/tree/{SWAY_GIT_TAG}/examples/{template}"); - - let template_url = Url::parse(&example_url)?; - - // If the user queried an existing example then continue otherwise attempt to fetch the examples and append them - // to the end of the error message so that the user can see the existing examples to choose from - match init_from_git_template(project_name, &template_url) { - Ok(()) => Ok(()), - Err(error) => { - let mut error_message = format!("Failed to initialize project from a template with the given name \"{template}\": {error}.\n Note: If you are attempting to initialize this project from a Sway example, please ensure the template name matches one of the available examples.\n"); - - let examples = match get_sway_examples() { - Ok(examples) => examples, - Err(err) => anyhow::bail!( - "{}\nFailed to fetch available examples: {}", - error_message, - err - ), - }; - - for example in examples { - error_message.push_str(format!("\t- {}\n", example).as_str()); - } - - anyhow::bail!("{}", error_message) - } - } - } - None => { - let program_type = match ( - command.contract, - command.script, - command.predicate, - command.library, - ) { - (_, false, false, false) => Contract, - (false, true, false, false) => Script, - (false, false, true, false) => Predicate, - (false, false, false, true) => Library, - _ => anyhow::bail!( - "Multiple types detected, please specify only one program type: \ + let program_type = match ( + command.contract, + command.script, + command.predicate, + command.library, + ) { + (_, false, false, false) => Contract, + (false, true, false, false) => Script, + (false, false, true, false) => Predicate, + (false, false, false, true) => Library, + _ => anyhow::bail!( + "Multiple types detected, please specify only one program type: \ \n Possible Types:\n - contract\n - script\n - predicate\n - library" - ), - }; - - init_new_project(project_name, program_type) - } - } -} -fn get_sway_examples() -> Result> { - // Query the main repo so that we can search for the "sha" that belongs to "examples" - let sway_response: GithubRepoResponse = ureq::get( - format!("https://api.github.com/repos/FuelLabs/sway/git/trees/{SWAY_GIT_TAG}").as_str(), - ) - .call()? - .into_json()?; - - // Filter out the URL that contains the "sha" for the next request - let examples_url = sway_response - .tree - .iter() - .filter(|tree| tree.path == "examples") - .map(|tree| tree.url.clone()) - .collect::(); - - // We want to store repo names of the "examples" that we have found - let mut examples: Vec = vec![]; - - if !examples_url.is_empty() { - let examples_response: GithubRepoResponse = ureq::get(&examples_url).call()?.into_json()?; - - // Filter out the repo names under "sway/examples" - examples = examples_response - .tree - .iter() - .map(|tree| tree.path.clone()) - .collect(); + ), }; - Ok(examples) + init_new_project(project_name, program_type) } pub(crate) fn init_new_project(project_name: String, program_type: ProgramType) -> Result<()> { @@ -271,231 +194,3 @@ pub(crate) fn init_new_project(project_name: String, program_type: ProgramType) Ok(()) } - -pub(crate) fn init_from_git_template(project_name: String, example_url: &Url) -> Result<()> { - let git = parse_github_link(example_url)?; - - let custom_url = format!( - "https://api.github.com/repos/{}/{}/contents/{}", - git.owner, git.repo_name, git.example_name - ); - - // Get the path of the example we are using - let path = std::env::current_dir()?; - let out_dir = path.join(&project_name); - let real_name = whoami::realname(); - - let responses: Vec = ureq::get(&custom_url).call()?.into_json()?; - - // Iterate through the responses to check that the link is a valid sway project - // by checking for a Forc.toml file. Otherwise, return an error - let valid_sway_project = responses - .iter() - .any(|response| response.name == "Forc.toml"); - if !valid_sway_project { - anyhow::bail!( - "The provided github URL: {} does not contain a Forc.toml file at the root", - example_url - ); - } - - // Download the files and directories from the github example - download_contents(&custom_url, &out_dir, &responses) - .with_context(|| format!("couldn't download from: {}", &custom_url))?; - - // Change the project name and authors of the Forc.toml file - edit_forc_toml(&out_dir, &project_name, &real_name)?; - - // If the example has a tests folder, edit the Cargo.toml - // Otherwise, create a basic tests template for the project - if out_dir.join("tests").exists() { - // Change the project name and authors of the Cargo.toml file - edit_cargo_toml(&out_dir, &project_name, &real_name)?; - } else { - // Create the tests directory, harness.rs and Cargo.toml file - fs::create_dir_all(out_dir.join("tests"))?; - - fs::write( - out_dir.join("tests").join("harness.rs"), - defaults::default_test_program(&project_name), - )?; - - fs::write( - out_dir.join("Cargo.toml"), - defaults::default_tests_manifest(&project_name), - )?; - } - - println_green(&format!("Successfully created: {}", project_name)); - - print_welcome_message(); - - Ok(()) -} - -fn parse_github_link(url: &Url) -> Result { - let mut path_segments = url.path_segments().context("cannot be base")?; - - let owner_name = path_segments - .next() - .context("Cannot parse owner name from github URL")?; - - let repo_name = path_segments - .next() - .context("Cannot repository name from github URL")?; - - let example_name = match path_segments - .skip(2) - .map(|s| s.to_string()) - .reduce(|cur: String, nxt: String| format!("{}/{}", cur, nxt)) - { - Some(example_name) => example_name, - None => "".to_string(), - }; - Ok(GitPathInfo { - owner: owner_name.to_string(), - repo_name: repo_name.to_string(), - example_name, - }) -} - -fn edit_forc_toml(out_dir: &Path, project_name: &str, real_name: &str) -> Result<()> { - let mut file = File::open(out_dir.join(constants::MANIFEST_FILE_NAME))?; - let mut toml = String::new(); - file.read_to_string(&mut toml)?; - let mut manifest_toml = toml.parse::()?; - - let mut authors = Vec::new(); - let forc_toml: toml::Value = toml::de::from_str(&toml)?; - if let Some(table) = forc_toml.as_table() { - if let Some(package) = table.get("project") { - // If authors Vec is currently populated use that - if let Some(toml::Value::Array(authors_vec)) = package.get("authors") { - for author in authors_vec { - if let toml::value::Value::String(name) = &author { - authors.push(name.clone()); - } - } - } - } - } - - // Only append the users name to the authors field if it isn't already in the list - if authors.iter().any(|e| e != real_name) { - authors.push(real_name.to_string()); - } - - let authors: toml_edit::Array = authors.iter().collect(); - manifest_toml["project"]["authors"] = toml_edit::value(authors); - manifest_toml["project"]["name"] = toml_edit::value(project_name); - - // Remove explicit std entry from copied template - if let Some(project) = manifest_toml.get_mut("dependencies") { - let _ = project - .as_table_mut() - .context("Unable to get forc manifest as table")? - .remove("std"); - } - - let mut file = File::create(out_dir.join(constants::MANIFEST_FILE_NAME))?; - file.write_all(manifest_toml.to_string().as_bytes())?; - Ok(()) -} - -fn edit_cargo_toml(out_dir: &Path, project_name: &str, real_name: &str) -> Result<()> { - let mut file = File::open(out_dir.join(constants::TEST_MANIFEST_FILE_NAME))?; - let mut toml = String::new(); - file.read_to_string(&mut toml)?; - - let mut updated_authors = toml_edit::Array::default(); - - let cargo_toml: toml::Value = toml::de::from_str(&toml)?; - if let Some(table) = cargo_toml.as_table() { - if let Some(package) = table.get("package") { - if let Some(toml::Value::Array(authors_vec)) = package.get("authors") { - for author in authors_vec { - if let toml::value::Value::String(name) = &author { - updated_authors.push(name); - } - } - } - } - } - updated_authors.push(real_name); - - let mut manifest_toml = toml.parse::()?; - manifest_toml["package"]["authors"] = toml_edit::value(updated_authors); - manifest_toml["package"]["name"] = toml_edit::value(project_name); - - let mut file = File::create(out_dir.join(constants::TEST_MANIFEST_FILE_NAME))?; - file.write_all(manifest_toml.to_string().as_bytes())?; - Ok(()) -} - -fn download_file(url: &str, file_name: &str, out_dir: &Path) -> Result { - let mut data = Vec::new(); - let resp = ureq::get(url).call()?; - resp.into_reader().read_to_end(&mut data)?; - let path = out_dir.canonicalize()?.join(file_name); - let mut file = File::create(&path)?; - file.write_all(&data[..])?; - Ok(path) -} - -fn download_contents(url: &str, out_dir: &Path, responses: &[ContentResponse]) -> Result<()> { - if !out_dir.exists() { - fs::create_dir(out_dir)?; - } - - // for all file_type == "file" responses, download the file and save it to the project directory. - // for all file_type == "dir" responses, recursively call this function. - for response in responses { - match &response.file_type { - FileType::File => { - if let Some(url) = &response.download_url { - download_file(url, &response.name, out_dir)?; - } - } - FileType::Dir => { - match &response.name.as_str() { - // Test directory no longer exists, make sure to create this from scratch!! - // Only download the directory and its contents if it matches src or tests - &constants::SRC_DIR | &constants::TEST_DIRECTORY => { - let dir = out_dir.join(&response.name); - let url = format!("{}/{}", url, response.name); - let responses: Vec = - ureq::get(&url).call()?.into_json()?; - download_contents(&url, &dir, &responses)?; - } - _ => (), - } - } - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::parse_github_link; - use url::Url; - - #[test] - fn test_github_link_parsing() { - let example_url = - Url::parse("https://github.com/FuelLabs/sway/tree/master/examples/hello_world") - .unwrap(); - let git = parse_github_link(&example_url).unwrap(); - assert_eq!(git.owner, "FuelLabs"); - assert_eq!(git.repo_name, "sway"); - assert_eq!(git.example_name, "examples/hello_world"); - - let example_url = - Url::parse("https://github.com/FuelLabs/swayswap-demo/tree/master/contracts").unwrap(); - let git = parse_github_link(&example_url).unwrap(); - assert_eq!(git.owner, "FuelLabs"); - assert_eq!(git.repo_name, "swayswap-demo"); - assert_eq!(git.example_name, "contracts"); - } -} diff --git a/forc/src/ops/forc_template.rs b/forc/src/ops/forc_template.rs new file mode 100644 index 00000000000..ee55654fdeb --- /dev/null +++ b/forc/src/ops/forc_template.rs @@ -0,0 +1,172 @@ +use crate::cli::TemplateCommand; +use crate::utils::{defaults, SWAY_GIT_TAG}; +use anyhow::{anyhow, Context, Result}; +use forc_pkg::{ + fetch_git, fetch_id, find_dir_within, git_commit_path, pin_git, Manifest, SourceGit, +}; +use forc_util::validate_name; +use fs_extra::dir::{copy, CopyOptions}; +use std::fs::File; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::{env, fs}; +use sway_utils::constants; +use tracing::info; +use url::Url; + +pub fn init(command: TemplateCommand) -> Result<()> { + validate_name(&command.project_name, "project name")?; + // The name used for the temporary local repo directory used for fetching the template. + let local_repo_name = match command.template_name.clone() { + Some(temp_name) => temp_name, + None => format!("{}-template-source", command.project_name), + }; + + let source = SourceGit { + repo: Url::parse(&command.url)?, + reference: forc_pkg::GitReference::DefaultBranch, + }; + + let current_dir = &env::current_dir()?; + + let fetch_ts = std::time::Instant::now(); + let fetch_id = fetch_id(current_dir, fetch_ts); + + info!("Resolving the HEAD of {}", source.repo); + let git_source = pin_git(fetch_id, &local_repo_name, source)?; + + let repo_path = git_commit_path( + &local_repo_name, + &git_source.source.repo, + &git_source.commit_hash, + ); + if !repo_path.exists() { + info!(" Fetching {}", git_source.to_string()); + fetch_git(fetch_id, &local_repo_name, &git_source)?; + } + + let from_path = match command.template_name { + Some(ref template_name) => find_dir_within(&repo_path, template_name, SWAY_GIT_TAG) + .ok_or_else(|| { + anyhow!( + "failed to find a template `{}` in {}", + template_name, + command.url + ) + })?, + None => { + let manifest_path = repo_path.join(constants::MANIFEST_FILE_NAME); + if Manifest::from_file(&manifest_path, SWAY_GIT_TAG).is_err() { + anyhow::bail!("failed to find a template in {}", command.url); + } + repo_path + } + }; + + // Create the target dir + let target_dir = current_dir.join(&command.project_name); + + info!("Creating {} from template", &command.project_name); + // Copy contents from template to target dir + copy_template_to_target(&from_path, &target_dir)?; + + // Edit forc.toml + edit_forc_toml(&target_dir, &command.project_name, &whoami::realname())?; + if target_dir.join("test").exists() { + edit_cargo_toml(&target_dir, &command.project_name, &whoami::realname())?; + } else { + // Create the tests directory, harness.rs and Cargo.toml file + fs::create_dir_all(target_dir.join("tests"))?; + + fs::write( + target_dir.join("tests").join("harness.rs"), + defaults::default_test_program(&command.project_name), + )?; + + fs::write( + target_dir.join("Cargo.toml"), + defaults::default_tests_manifest(&command.project_name), + )?; + } + Ok(()) +} + +fn edit_forc_toml(out_dir: &Path, project_name: &str, real_name: &str) -> Result<()> { + let mut file = File::open(out_dir.join(constants::MANIFEST_FILE_NAME))?; + let mut toml = String::new(); + file.read_to_string(&mut toml)?; + let mut manifest_toml = toml.parse::()?; + + let mut authors = Vec::new(); + let forc_toml: toml::Value = toml::de::from_str(&toml)?; + if let Some(table) = forc_toml.as_table() { + if let Some(package) = table.get("project") { + // If authors Vec is currently populated use that + if let Some(toml::Value::Array(authors_vec)) = package.get("authors") { + for author in authors_vec { + if let toml::value::Value::String(name) = &author { + authors.push(name.clone()); + } + } + } + } + } + + // Only append the users name to the authors field if it isn't already in the list + if authors.iter().any(|e| e != real_name) { + authors.push(real_name.to_string()); + } + + let authors: toml_edit::Array = authors.iter().collect(); + manifest_toml["project"]["authors"] = toml_edit::value(authors); + manifest_toml["project"]["name"] = toml_edit::value(project_name); + + // Remove explicit std entry from copied template + if let Some(project) = manifest_toml.get_mut("dependencies") { + let _ = project + .as_table_mut() + .context("Unable to get forc manifest as table")? + .remove("std"); + } + + let mut file = File::create(out_dir.join(constants::MANIFEST_FILE_NAME))?; + file.write_all(manifest_toml.to_string().as_bytes())?; + Ok(()) +} + +fn edit_cargo_toml(out_dir: &Path, project_name: &str, real_name: &str) -> Result<()> { + let mut file = File::open(out_dir.join(constants::TEST_MANIFEST_FILE_NAME))?; + let mut toml = String::new(); + file.read_to_string(&mut toml)?; + + let mut updated_authors = toml_edit::Array::default(); + + let cargo_toml: toml::Value = toml::de::from_str(&toml)?; + if let Some(table) = cargo_toml.as_table() { + if let Some(package) = table.get("package") { + if let Some(toml::Value::Array(authors_vec)) = package.get("authors") { + for author in authors_vec { + if let toml::value::Value::String(name) = &author { + updated_authors.push(name); + } + } + } + } + } + updated_authors.push(real_name); + + let mut manifest_toml = toml.parse::()?; + manifest_toml["package"]["authors"] = toml_edit::value(updated_authors); + manifest_toml["package"]["name"] = toml_edit::value(project_name); + + let mut file = File::create(out_dir.join(constants::TEST_MANIFEST_FILE_NAME))?; + file.write_all(manifest_toml.to_string().as_bytes())?; + Ok(()) +} + +fn copy_template_to_target(from: &PathBuf, to: &PathBuf) -> Result<()> { + let mut copy_options = CopyOptions::new(); + copy_options.copy_inside = true; + copy(from, to, ©_options)?; + Ok(()) +} diff --git a/forc/src/ops/mod.rs b/forc/src/ops/mod.rs index af17a00c0ba..1d5acc42f28 100644 --- a/forc/src/ops/mod.rs +++ b/forc/src/ops/mod.rs @@ -4,4 +4,5 @@ pub mod forc_clean; pub mod forc_deploy; pub mod forc_init; pub mod forc_run; +pub mod forc_template; pub mod forc_update; diff --git a/scripts/mdbook-forc-documenter/examples/forc_template.md b/scripts/mdbook-forc-documenter/examples/forc_template.md new file mode 100644 index 00000000000..61cd9814772 --- /dev/null +++ b/scripts/mdbook-forc-documenter/examples/forc_template.md @@ -0,0 +1,16 @@ + +## EXAMPLE: + + +```sh +forc template --url https://github.com/owner/template/ --project_name my_example_project +``` + +The command above fetches the HEAD of the `template` repo and searchs for `Forc.toml` at the root of the fetched repo. It will fetch the repo and preapare a new `Forc.toml` with new project name. Outputs everthing to `current_dir/project_name`. + + +```sh +forc template --url https://github.com/FuelLabs/sway/ --template_name counter --project_name my_example_project +``` + +The command above fetches the HEAD of the `sway` repo and searchs for `counter` example inside it (There is an example called `counter` under `sway/examples`). It will fetch the `counter` example and preapare a new `Forc.toml` with new project name. Outputs everthing to `current_dir/project_name`..