anchor/cli/src/main.rs

392 lines
12 KiB
Rust

use crate::config::{find_cargo_toml, read_all_programs, Config, Program};
use anchor_syn::idl::Idl;
use anyhow::Result;
use clap::Clap;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::process::{Child, Stdio};
use std::string::ToString;
mod config;
mod template;
#[derive(Debug, Clap)]
pub struct Opts {
#[clap(subcommand)]
pub command: Command,
}
#[derive(Debug, Clap)]
pub enum Command {
/// Initializes a workspace.
Init { name: String },
/// Builds a Solana program.
Build {
/// Output directory for the IDL.
#[clap(short, long)]
idl: Option<String>,
},
/// Runs integration tests against a localnetwork.
Test,
/// Creates a new program.
New { name: String },
/// Outputs an interface definition file.
Idl {
/// Path to the program's interface definition.
#[clap(short, long)]
file: String,
/// Output file for the idl (stdout if not specified).
#[clap(short, long)]
out: Option<String>,
},
}
fn main() -> Result<()> {
let opts = Opts::parse();
match opts.command {
Command::Init { name } => init(name),
Command::Build { idl } => build(idl),
Command::Test => test(),
Command::New { name } => new(name),
Command::Idl { file, out } => {
if out.is_none() {
return idl(file, None);
}
idl(file, Some(&PathBuf::from(out.unwrap())))
}
}
}
fn init(name: String) -> Result<()> {
let cfg = Config::discover()?;
if cfg.is_some() {
println!("Anchor workspace already initialized");
}
fs::create_dir(name.clone())?;
std::env::set_current_dir(&name)?;
fs::create_dir("app")?;
let cfg = Config::default();
let toml = cfg.to_string();
let mut file = File::create("Anchor.toml")?;
file.write_all(toml.as_bytes())?;
// Build virtual manifest.
let mut virt_manifest = File::create("Cargo.toml")?;
virt_manifest.write_all(template::virtual_manifest().as_bytes())?;
// Build the program.
fs::create_dir("programs")?;
new_program(&name)?;
// Build the test suite.
fs::create_dir("tests")?;
let mut mocha = File::create(&format!("tests/{}.js", name))?;
mocha.write_all(template::mocha(&name).as_bytes())?;
println!("{} initialized", name);
Ok(())
}
// Creates a new program crate in the `programs/<name>` directory.
fn new(name: String) -> Result<()> {
match Config::discover()? {
None => {
println!("Not in anchor workspace.");
std::process::exit(1);
}
Some((_cfg, cfg_path, _inside_cargo)) => {
match cfg_path.parent() {
None => {
println!("Unable to make new program");
}
Some(parent) => {
std::env::set_current_dir(&parent)?;
new_program(&name)?;
println!("Created new program.");
}
};
}
}
Ok(())
}
// Creates a new program crate in the current directory with `name`.
fn new_program(name: &str) -> Result<()> {
fs::create_dir(&format!("programs/{}", name))?;
fs::create_dir(&format!("programs/{}/src/", name))?;
let mut cargo_toml = File::create(&format!("programs/{}/Cargo.toml", name))?;
cargo_toml.write_all(template::cargo_toml(&name).as_bytes())?;
let mut xargo_toml = File::create(&format!("programs/{}/Xargo.toml", name))?;
xargo_toml.write_all(template::xargo_toml(&name).as_bytes())?;
let mut lib_rs = File::create(&format!("programs/{}/src/lib.rs", name))?;
lib_rs.write_all(template::lib_rs(&name).as_bytes())?;
Ok(())
}
fn build(idl: Option<String>) -> Result<()> {
match Config::discover()? {
None => build_cwd(idl),
Some((cfg, cfg_path, inside_cargo)) => build_ws(cfg, cfg_path, inside_cargo, idl),
}
}
// Runs the build inside a workspace.
//
// * Builds a single program if the current dir is within a Cargo subdirectory,
// e.g., `programs/my-program/src`.
// * Builds *all* programs if thje current dir is anywhere else in the workspace.
//
fn build_ws(
cfg: Config,
cfg_path: PathBuf,
cargo_toml: Option<PathBuf>,
idl: Option<String>,
) -> Result<()> {
let idl_out = match idl {
Some(idl) => Some(PathBuf::from(idl)),
None => {
let cfg_parent = match cfg_path.parent() {
None => return Err(anyhow::anyhow!("Invalid Anchor.toml")),
Some(parent) => parent,
};
fs::create_dir_all(cfg_parent.join("target/idl"))?;
Some(cfg_parent.join("target/idl"))
}
};
match cargo_toml {
None => build_all(cfg, cfg_path, idl_out),
Some(ct) => _build_cwd(ct, idl_out),
}
}
fn build_all(_cfg: Config, cfg_path: PathBuf, idl_out: Option<PathBuf>) -> Result<()> {
match cfg_path.parent() {
None => Err(anyhow::anyhow!(
"Invalid Anchor.toml at {}",
cfg_path.display()
)),
Some(parent) => {
let files = fs::read_dir(parent.join("programs"))?;
for f in files {
let p = f?.path();
_build_cwd(p.join("Cargo.toml"), idl_out.clone())?;
}
Ok(())
}
}
}
fn build_cwd(idl_out: Option<String>) -> Result<()> {
match find_cargo_toml()? {
None => {
println!("Cargo.toml not found");
std::process::exit(1);
}
Some(cargo_toml) => _build_cwd(cargo_toml, idl_out.map(PathBuf::from)),
}
}
// Runs the build command outside of a workspace.
fn _build_cwd(cargo_toml: PathBuf, idl_out: Option<PathBuf>) -> Result<()> {
match cargo_toml.parent() {
None => return Err(anyhow::anyhow!("Unable to find parent")),
Some(p) => std::env::set_current_dir(&p)?,
};
let exit = std::process::Command::new("cargo")
.arg("build-bpf")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
if !exit.status.success() {
std::process::exit(exit.status.code().unwrap_or(1));
}
// Always assume idl is located ar src/lib.rs.
let idl = extract_idl("src/lib.rs")?;
let out = match idl_out {
None => PathBuf::from(".").join(&idl.name).with_extension("json"),
Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("json")),
};
write_idl(&idl, Some(&out))
}
fn idl(file: String, out: Option<&Path>) -> Result<()> {
let idl = extract_idl(&file)?;
write_idl(&idl, out)
}
fn extract_idl(file: &str) -> Result<Idl> {
let file = shellexpand::tilde(file);
anchor_syn::parser::file::parse(&*file)
}
fn write_idl(idl: &Idl, out: Option<&Path>) -> Result<()> {
let idl_json = serde_json::to_string_pretty(idl)?;
match out.as_ref() {
None => println!("{}", idl_json),
Some(out) => std::fs::write(out, idl_json)?,
};
Ok(())
}
// Builds, deploys, and tests all workspace programs in a single command.
fn test() -> Result<()> {
// Switch directories to top level workspace.
set_workspace_dir_or_exit();
// Build everything.
build(None)?;
// Switch again (todo: restore cwd in `build` command).
set_workspace_dir_or_exit();
// Bootup validator.
let mut validator_handle = start_test_validator()?;
// Deploy all programs.
let programs = deploy_ws()?;
// Store deployed program addresses in IDL metadata (for consumption by
// client + tests).
for (program, address) in programs {
// Add metadata to the IDL.
let mut idl = program.idl;
idl.metadata = Some(serde_json::to_value(IdlTestMetadata {
address: address.to_string(),
})?);
// Persist it.
let idl_out = PathBuf::from("target/idl")
.join(&idl.name)
.with_extension("json");
write_idl(&idl, Some(&idl_out))?;
}
// Run the tests.
if let Err(e) = std::process::Command::new("mocha")
.arg("tests/")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
{
validator_handle.kill()?;
return Err(anyhow::format_err!("{}", e.to_string()));
}
validator_handle.kill()?;
Ok(())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IdlTestMetadata {
address: String,
}
fn start_test_validator() -> Result<Child> {
fs::create_dir_all(".anchor")?;
let test_ledger_filename = ".anchor/test-ledger";
let test_ledger_log_filename = ".anchor/test-ledger-log.txt";
if Path::new(test_ledger_filename).exists() {
std::fs::remove_dir_all(test_ledger_filename)?;
}
if Path::new(test_ledger_log_filename).exists() {
std::fs::remove_file(test_ledger_log_filename)?;
}
// Start a validator for testing.
let test_validator_stdout = File::create(test_ledger_log_filename)?;
let test_validator_stderr = test_validator_stdout.try_clone()?;
let validator_handle = std::process::Command::new("solana-test-validator")
.arg("--ledger")
.arg(test_ledger_filename)
.stdout(Stdio::from(test_validator_stdout))
.stderr(Stdio::from(test_validator_stderr))
.spawn()
.map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
// TODO: do something more sensible than sleeping.
std::thread::sleep(std::time::Duration::from_millis(2000));
Ok(validator_handle)
}
fn deploy_ws() -> Result<Vec<(Program, Pubkey)>> {
let mut programs = vec![];
println!("Deploying workspace to http://localhost:8899...");
for program in read_all_programs()? {
let binary_path = format!(
"target/bpfel-unknown-unknown/release/{}.so",
program.lib_name
);
println!("Deploying {}...", binary_path);
let exit = std::process::Command::new("solana")
.arg("deploy")
.arg(&binary_path)
.arg("--url")
.arg("http://localhost:8899") // TODO: specify network via cli.
.arg("--keypair")
.arg(".anchor/test-ledger/faucet-keypair.json") // TODO: specify wallet.
.output()
.expect("Must deploy");
if !exit.status.success() {
println!("There was a problem deploying.");
std::process::exit(exit.status.code().unwrap_or(1));
}
let stdout: DeployStdout = serde_json::from_str(std::str::from_utf8(&exit.stdout)?)?;
programs.push((program, stdout.program_id.parse()?));
}
println!("Deploy success!");
Ok(programs)
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeployStdout {
program_id: String,
}
fn set_workspace_dir_or_exit() {
let d = match Config::discover() {
Err(_) => {
println!("Not in anchor workspace.");
std::process::exit(1);
}
Ok(d) => d,
};
match d {
None => {
println!("Not in anchor workspace.");
std::process::exit(1);
}
Some((_cfg, cfg_path, _inside_cargo)) => {
match cfg_path.parent() {
None => {
println!("Unable to make new program");
}
Some(parent) => match std::env::set_current_dir(&parent) {
Err(_) => {
println!("Not in anchor workspace.");
std::process::exit(1);
}
Ok(_) => {}
},
};
}
}
}