cli: Add program template with multiple files (#2602)

This commit is contained in:
acheron 2023-08-15 23:58:17 +02:00 committed by GitHub
parent 454f1dd044
commit 6eacad4b11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 266 additions and 108 deletions

View File

@ -20,7 +20,8 @@ The minor version will be incremented upon a breaking change and the patch versi
- ts: Add ability to access workspace programs independent of the casing used, e.g. `anchor.workspace.myProgram`, `anchor.workspace.MyProgram`... ([#2579](https://github.com/coral-xyz/anchor/pull/2579)).
- spl: Export `mpl-token-metadata` crate ([#2583](https://github.com/coral-xyz/anchor/pull/2583)).
- spl: Add `TokenRecordAccount` for pNFTs ([#2597](https://github.com/coral-xyz/anchor/pull/2597)).
- ts: Add support for unnamed(tuple) enum in accounts([#2601](https://github.com/coral-xyz/anchor/pull/2601)).
- ts: Add support for unnamed(tuple) enum in accounts ([#2601](https://github.com/coral-xyz/anchor/pull/2601)).
- cli: Add program template with multiple files for instructions, state... ([#2602](https://github.com/coral-xyz/anchor/pull/2602)).
### Fixes

View File

@ -20,6 +20,7 @@ use heck::{ToKebabCase, ToSnakeCase};
use regex::{Regex, RegexBuilder};
use reqwest::blocking::multipart::{Form, Part};
use reqwest::blocking::Client;
use rust_template::ProgramTemplate;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value as JsonValue};
@ -69,15 +70,23 @@ pub struct Opts {
pub enum Command {
/// Initializes a workspace.
Init {
/// Workspace name
name: String,
/// Use JavaScript instead of TypeScript
#[clap(short, long)]
javascript: bool,
/// Use Solidity instead of Rust
#[clap(short, long)]
solidity: bool,
/// Don't initialize git
#[clap(long)]
no_git: bool,
/// Use `jest` instead of `mocha` for tests
#[clap(long)]
jest: bool,
/// Rust program template to use
#[clap(value_enum, short, long, default_value = "single")]
template: ProgramTemplate,
},
/// Builds the workspace.
#[clap(name = "build", alias = "b")]
@ -207,9 +216,14 @@ pub enum Command {
},
/// Creates a new program.
New {
/// Program name
name: String,
/// Use Solidity instead of Rust
#[clap(short, long)]
solidity: bool,
name: String,
/// Rust program template to use
#[clap(value_enum, short, long, default_value = "single")]
template: ProgramTemplate,
},
/// Commands for interacting with interface definitions.
Idl {
@ -456,8 +470,21 @@ pub fn entry(opts: Opts) -> Result<()> {
solidity,
no_git,
jest,
} => init(&opts.cfg_override, name, javascript, solidity, no_git, jest),
Command::New { solidity, name } => new(&opts.cfg_override, solidity, name),
template,
} => init(
&opts.cfg_override,
name,
javascript,
solidity,
no_git,
jest,
template,
),
Command::New {
solidity,
name,
template,
} => new(&opts.cfg_override, solidity, name, template),
Command::Build {
idl,
idl_ts,
@ -604,6 +631,7 @@ fn init(
solidity: bool,
no_git: bool,
jest: bool,
template: ProgramTemplate,
) -> Result<()> {
if Config::discover(cfg_override)?.is_some() {
return Err(anyhow!("Workspace already initialized"));
@ -682,17 +710,11 @@ fn init(
// Build the program.
if solidity {
fs::create_dir("solidity")?;
new_solidity_program(&project_name)?;
solidity_template::create_program(&project_name)?;
} else {
// Build virtual manifest for rust programs
fs::write("Cargo.toml", rust_template::virtual_manifest())?;
fs::create_dir("programs")?;
new_rust_program(&project_name)?;
rust_template::create_program(&project_name, template)?;
}
// Build the test suite.
fs::create_dir("tests")?;
// Build the migrations directory.
@ -783,7 +805,12 @@ fn install_node_modules(cmd: &str) -> Result<std::process::Output> {
}
// Creates a new program crate in the `programs/<name>` directory.
fn new(cfg_override: &ConfigOverride, solidity: bool, name: String) -> Result<()> {
fn new(
cfg_override: &ConfigOverride,
solidity: bool,
name: String,
template: ProgramTemplate,
) -> Result<()> {
with_workspace(cfg_override, |cfg| {
match cfg.path().parent() {
None => {
@ -802,10 +829,10 @@ fn new(cfg_override: &ConfigOverride, solidity: bool, name: String) -> Result<()
name.clone(),
ProgramDeployment {
address: if solidity {
new_solidity_program(&name)?;
solidity_template::create_program(&name)?;
solidity_template::default_program_id()
} else {
new_rust_program(&name)?;
rust_template::create_program(&name, template)?;
rust_template::get_or_create_program_id(&name)
},
path: None,
@ -823,26 +850,32 @@ fn new(cfg_override: &ConfigOverride, solidity: bool, name: String) -> Result<()
})
}
// Creates a new rust program crate in the current directory with `name`.
fn new_rust_program(name: &str) -> Result<()> {
if !PathBuf::from("Cargo.toml").exists() {
fs::write("Cargo.toml", rust_template::virtual_manifest())?;
}
fs::create_dir_all(format!("programs/{name}/src/"))?;
let mut cargo_toml = File::create(format!("programs/{name}/Cargo.toml"))?;
cargo_toml.write_all(rust_template::cargo_toml(name).as_bytes())?;
let mut xargo_toml = File::create(format!("programs/{name}/Xargo.toml"))?;
xargo_toml.write_all(rust_template::xargo_toml().as_bytes())?;
let mut lib_rs = File::create(format!("programs/{name}/src/lib.rs"))?;
lib_rs.write_all(rust_template::lib_rs(name).as_bytes())?;
Ok(())
}
/// Array of (path, content) tuple.
pub type Files = Vec<(PathBuf, String)>;
/// Create files from the given (path, content) tuple array.
///
/// # Example
///
/// ```ignore
/// crate_files(vec![("programs/my_program/src/lib.rs".into(), "// Content".into())])?;
/// ```
pub fn create_files(files: &Files) -> Result<()> {
for (path, content) in files {
let path = Path::new(path);
if path.exists() {
continue;
}
match path.extension() {
Some(_) => {
fs::create_dir_all(path.parent().unwrap())?;
fs::write(path, content)?;
}
None => fs::create_dir_all(path)?,
}
}
// Creates a new solidity program in the current directory with `name`.
fn new_solidity_program(name: &str) -> Result<()> {
fs::create_dir_all("solidity")?;
let mut lib_rs = File::create(format!("solidity/{name}.sol"))?;
lib_rs.write_all(solidity_template::solidity(name).as_bytes())?;
Ok(())
}
@ -4224,6 +4257,7 @@ mod tests {
false,
false,
false,
ProgramTemplate::default(),
)
.unwrap();
}
@ -4241,6 +4275,7 @@ mod tests {
false,
false,
false,
ProgramTemplate::default(),
)
.unwrap();
}
@ -4258,6 +4293,7 @@ mod tests {
false,
false,
false,
ProgramTemplate::default(),
)
.unwrap();
}

View File

@ -1,7 +1,8 @@
use crate::config::ProgramWorkspace;
use crate::VERSION;
use crate::{config::ProgramWorkspace, create_files, Files};
use anchor_syn::idl::types::Idl;
use anyhow::Result;
use clap::{Parser, ValueEnum};
use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
use solana_sdk::{
pubkey::Pubkey,
@ -10,22 +11,140 @@ use solana_sdk::{
};
use std::{fmt::Write, path::Path};
/// Read the program keypair file or create a new one if it doesn't exist.
pub fn get_or_create_program_id(name: &str) -> Pubkey {
let keypair_path = Path::new("target")
.join("deploy")
.join(format!("{}-keypair.json", name.to_snake_case()));
read_keypair_file(&keypair_path)
.unwrap_or_else(|_| {
let keypair = Keypair::new();
write_keypair_file(&keypair, keypair_path).expect("Unable to create program keypair");
keypair
})
.pubkey()
/// Program initialization template
#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum)]
pub enum ProgramTemplate {
/// Program with a single `lib.rs` file
#[default]
Single,
/// Program with multiple files for instructions, state...
Multiple,
}
pub fn virtual_manifest() -> &'static str {
/// Create a program from the given name and template.
pub fn create_program(name: &str, template: ProgramTemplate) -> Result<()> {
let program_path = Path::new("programs").join(name);
let common_files = vec![
("Cargo.toml".into(), workspace_manifest().into()),
(program_path.join("Cargo.toml"), cargo_toml(name)),
(program_path.join("Xargo.toml"), xargo_toml().into()),
];
let template_files = match template {
ProgramTemplate::Single => create_program_template_single(name, &program_path),
ProgramTemplate::Multiple => create_program_template_multiple(name, &program_path),
};
create_files(&[common_files, template_files].concat())
}
/// Create a program with a single `lib.rs` file.
fn create_program_template_single(name: &str, program_path: &Path) -> Files {
vec![(
program_path.join("src").join("lib.rs"),
format!(
r#"use anchor_lang::prelude::*;
declare_id!("{}");
#[program]
pub mod {} {{
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
Ok(())
}}
}}
#[derive(Accounts)]
pub struct Initialize {{}}
"#,
get_or_create_program_id(name),
name.to_snake_case(),
),
)]
}
/// Create a program with multiple files for instructions, state...
fn create_program_template_multiple(name: &str, program_path: &Path) -> Files {
let src_path = program_path.join("src");
vec![
(
src_path.join("lib.rs"),
format!(
r#"pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;
use anchor_lang::prelude::*;
pub use constants::*;
pub use instructions::*;
pub use state::*;
declare_id!("{}");
#[program]
pub mod {} {{
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
initialize::handler(ctx)
}}
}}
"#,
get_or_create_program_id(name),
name.to_snake_case(),
),
),
(
src_path.join("constants.rs"),
r#"use anchor_lang::prelude::*;
#[constant]
pub const SEED: &str = "anchor";
"#
.into(),
),
(
src_path.join("error.rs"),
r#"use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Custom error message")]
CustomError,
}
"#
.into(),
),
(
src_path.join("instructions").join("mod.rs"),
r#"pub mod initialize;
pub use initialize::*;
"#
.into(),
),
(
src_path.join("instructions").join("initialize.rs"),
r#"use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct Initialize {}
pub fn handler(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
"#
.into(),
),
(src_path.join("state").join("mod.rs"), r#""#.into()),
]
}
const fn workspace_manifest() -> &'static str {
r#"[workspace]
members = [
"programs/*"
@ -42,6 +161,55 @@ codegen-units = 1
"#
}
fn cargo_toml(name: &str) -> String {
format!(
r#"[package]
name = "{0}"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "{1}"
[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
anchor-lang = "{2}"
"#,
name,
name.to_snake_case(),
VERSION,
)
}
fn xargo_toml() -> &'static str {
r#"[target.bpfel-unknown-unknown.dependencies.std]
features = []
"#
}
/// Read the program keypair file or create a new one if it doesn't exist.
pub fn get_or_create_program_id(name: &str) -> Pubkey {
let keypair_path = Path::new("target")
.join("deploy")
.join(format!("{}-keypair.json", name.to_snake_case()));
read_keypair_file(&keypair_path)
.unwrap_or_else(|_| {
let keypair = Keypair::new();
write_keypair_file(&keypair, keypair_path).expect("Unable to create program keypair");
keypair
})
.pubkey()
}
pub fn credentials(token: &str) -> String {
format!(
r#"[registry]
@ -68,34 +236,6 @@ export const IDL: {} = {};
))
}
pub fn cargo_toml(name: &str) -> String {
format!(
r#"[package]
name = "{0}"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "{1}"
[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
anchor-lang = "{2}"
"#,
name,
name.to_snake_case(),
VERSION,
)
}
pub fn deploy_js_script_host(cluster_url: &str, script_path: &str) -> String {
format!(
r#"
@ -181,35 +321,6 @@ module.exports = async function (provider) {
"#
}
pub fn xargo_toml() -> &'static str {
r#"[target.bpfel-unknown-unknown.dependencies.std]
features = []
"#
}
pub fn lib_rs(name: &str) -> String {
format!(
r#"use anchor_lang::prelude::*;
declare_id!("{}");
#[program]
pub mod {} {{
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
Ok(())
}}
}}
#[derive(Accounts)]
pub struct Initialize {{}}
"#,
get_or_create_program_id(name),
name.to_snake_case(),
)
}
pub fn mocha(name: &str) -> String {
format!(
r#"const anchor = require("@coral-xyz/anchor");

View File

@ -1,10 +1,20 @@
use crate::config::ProgramWorkspace;
use crate::VERSION;
use crate::{config::ProgramWorkspace, create_files};
use anchor_syn::idl::types::Idl;
use anyhow::Result;
use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
use solana_sdk::pubkey::Pubkey;
use std::fmt::Write;
use std::path::Path;
/// Create a solidity program.
pub fn create_program(name: &str) -> Result<()> {
let files = vec![(
Path::new("solidity").join(name).with_extension("sol"),
solidity(name),
)];
create_files(&files)
}
pub fn default_program_id() -> Pubkey {
"F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC"