cli: Add program template with multiple files (#2602)
This commit is contained in:
parent
454f1dd044
commit
6eacad4b11
|
@ -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
|
||||
|
||||
|
|
104
cli/src/lib.rs
104
cli/src/lib.rs
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue