diff --git a/CHANGELOG.md b/CHANGELOG.md index 524fff9ab6..f1f437d423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,7 @@ The minor version will be incremented upon a breaking change and the patch versi * lang: Add `realloc`, `realloc::payer`, and `realloc::zero` as a new constraint group for program accounts ([#1943](https://github.com/project-serum/anchor/pull/1943)). * lang: Add `PartialEq` and `Eq` for `anchor_lang::Error` ([#1544](https://github.com/project-serum/anchor/pull/1544)). -* cli: Add `--skip-build` to `anchor publish` ([#1786](https://github. -com/project-serum/anchor/pull/1841)). +* cli: Add `--skip-build` to `anchor publish` ([#1841](https://github.com/project-serum/anchor/pull/1841)). * cli: Add `--program-keypair` to `anchor deploy` ([#1786](https://github.com/project-serum/anchor/pull/1786)). * cli: Add compilation optimizations to cli template ([#1807](https://github.com/project-serum/anchor/pull/1807)). * cli: `build` now adds docs to idl. This can be turned off with `--no-docs` ([#1561](https://github.com/project-serum/anchor/pull/1561)). @@ -27,6 +26,7 @@ com/project-serum/anchor/pull/1841)). * ts: Add `program.coder.types` for encoding/decoding user-defined types ([#1931](https://github.com/project-serum/anchor/pull/1931)). * client: Add send_with_spinner_and_config function to RequestBuilder ([#1926](https://github.com/project-serum/anchor/pull/1926)). * ts: Implement a coder for SPL associated token program ([#1939](https://github.com/project-serum/anchor/pull/1939)). +* cli: Add support for CLI hooks (build/test/deploy) ([#1796](https://github.com/project-serum/anchor/pull/1796)). ### Fixes diff --git a/cli/src/config.rs b/cli/src/config.rs index fc5cddd669..1cc78e89d7 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -274,6 +274,7 @@ pub struct Config { pub provider: ProviderConfig, pub programs: ProgramsConfig, pub scripts: ScriptsConfig, + pub hooks: HooksConfig, pub workspace: WorkspaceConfig, // Separate entry next to test_config because // "anchor localnet" only has access to the Anchor.toml, @@ -309,6 +310,72 @@ pub struct ProviderConfig { pub type ScriptsConfig = BTreeMap; +#[derive(Default, Clone, Debug, Serialize, Deserialize)] +pub struct HooksConfig { + pub pre_build: Option, + pub post_build: Option, + pub pre_test: Option, + pub post_test: Option, + pub pre_deploy: Option, + pub post_deploy: Option, +} + +const VALID_HOOKS: [&str; 6] = [ + "pre_build", + "post_build", + "pre_test", + "post_test", + "pre_deploy", + "post_deploy", +]; + +impl From<_HooksConfig> for HooksConfig { + fn from(s: _HooksConfig) -> Self { + let mut invalid_hooks = s.clone(); + invalid_hooks.retain(|k, _| !VALID_HOOKS.contains(&k.as_str())); + if !invalid_hooks.is_empty() { + panic!( + "Error reading hooks. Hooks {:?} are not part of valid hooks: {:?}.", + invalid_hooks.keys(), + VALID_HOOKS + ) + } + HooksConfig { + pre_build: s.get("pre_build").map(Clone::clone), + post_build: s.get("post_build").map(Clone::clone), + pre_test: s.get("pre_test").map(Clone::clone), + post_test: s.get("post_test").map(Clone::clone), + pre_deploy: s.get("pre_deploy").map(Clone::clone), + post_deploy: s.get("post_deploy").map(Clone::clone), + } + } +} + +impl From for _HooksConfig { + fn from(s: HooksConfig) -> Self { + let mut hooks = _HooksConfig::default(); + if let Some(pre_build) = s.pre_build { + hooks.insert("pre_build".to_string(), pre_build); + } + if let Some(post_build) = s.post_build { + hooks.insert("post_build".to_string(), post_build); + } + if let Some(pre_test) = s.pre_test { + hooks.insert("pre_test".to_string(), pre_test); + } + if let Some(post_test) = s.post_test { + hooks.insert("post_test".to_string(), post_test); + } + if let Some(pre_deploy) = s.pre_deploy { + hooks.insert("pre_deploy".to_string(), pre_deploy); + } + if let Some(post_deploy) = s.post_deploy { + hooks.insert("post_deploy".to_string(), post_deploy); + } + hooks + } +} + pub type ProgramsConfig = BTreeMap>; #[derive(Debug, Default, Clone, Serialize, Deserialize)] @@ -403,6 +470,8 @@ impl Config { } } +pub type _HooksConfig = BTreeMap; + #[derive(Debug, Serialize, Deserialize)] struct _Config { anchor_version: Option, @@ -413,6 +482,7 @@ struct _Config { provider: Provider, workspace: Option, scripts: Option, + hooks: Option<_HooksConfig>, test: Option<_TestValidator>, } @@ -446,6 +516,7 @@ impl ToString for Config { true => None, false => Some(self.scripts.clone()), }, + hooks: Some(self.hooks.clone().into()), programs, workspace: (!self.workspace.members.is_empty() || !self.workspace.exclude.is_empty()) .then(|| self.workspace.clone()), @@ -471,6 +542,7 @@ impl FromStr for Config { wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?, }, scripts: cfg.scripts.unwrap_or_default(), + hooks: cfg.hooks.unwrap_or_default().into(), test_validator: cfg.test.map(Into::into), test_config: None, programs: cfg.programs.map_or(Ok(BTreeMap::new()), deser_programs)?, diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 505a469c9e..8924cccd2a 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -773,6 +773,9 @@ pub fn build( } let cfg = Config::discover(cfg_override)?.expect("Not in workspace."); + + try_run_hook(&cfg, HookType::PreBuild)?; + let build_config = BuildConfig { verifiable, solana_version: solana_version.or_else(|| cfg.solana_version.clone()), @@ -843,6 +846,8 @@ pub fn build( set_workspace_dir_or_exit(); + try_run_hook(&cfg, HookType::PostBuild)?; + Ok(()) } @@ -1894,6 +1899,9 @@ fn test( if (!is_localnet || skip_local_validator) && !skip_deploy { deploy(cfg_override, None, None)?; } + + try_run_hook(cfg, HookType::PreTest)?; + let mut is_first_suite = true; if cfg.scripts.get("test").is_some() { is_first_suite = false; @@ -1938,6 +1946,9 @@ fn test( )?; } } + + try_run_hook(cfg, HookType::PostTest)?; + Ok(()) }) } @@ -2403,6 +2414,8 @@ fn deploy( let url = cluster_url(cfg, &cfg.test_validator); let keypair = cfg.provider.wallet.to_string(); + try_run_hook(cfg, HookType::PreDeploy)?; + // Deploy the programs. println!("Deploying workspace: {}", url); println!("Upgrade authority: {}", keypair); @@ -2464,6 +2477,8 @@ fn deploy( println!("Deploy success"); + try_run_hook(cfg, HookType::PostDeploy)?; + Ok(()) }) } @@ -2813,28 +2828,58 @@ fn shell(cfg_override: &ConfigOverride) -> Result<()> { fn run(cfg_override: &ConfigOverride, script: String, script_args: Vec) -> Result<()> { with_workspace(cfg_override, |cfg| { - let url = cluster_url(cfg, &cfg.test_validator); let script = cfg .scripts .get(&script) .ok_or_else(|| anyhow!("Unable to find script"))?; let script_with_args = format!("{script} {}", script_args.join(" ")); - let exit = std::process::Command::new("bash") - .arg("-c") - .arg(&script_with_args) - .env("ANCHOR_PROVIDER_URL", url) - .env("ANCHOR_WALLET", cfg.provider.wallet.to_string()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .output() - .unwrap(); - if !exit.status.success() { - std::process::exit(exit.status.code().unwrap_or(1)); - } + run_bash_cmd(cfg, &script_with_args)?; Ok(()) }) } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum HookType { + PreBuild, + PostBuild, + PreTest, + PostTest, + PreDeploy, + PostDeploy, +} + +fn try_run_hook(cfg: &WithPath, hook_type: HookType) -> Result<()> { + let cmd = match hook_type { + HookType::PreBuild => &cfg.hooks.pre_build, + HookType::PostBuild => &cfg.hooks.post_build, + HookType::PreTest => &cfg.hooks.pre_test, + HookType::PostTest => &cfg.hooks.post_test, + HookType::PreDeploy => &cfg.hooks.pre_deploy, + HookType::PostDeploy => &cfg.hooks.post_deploy, + }; + if let Some(cmd) = &cmd { + run_bash_cmd(cfg, cmd)?; + }; + Ok(()) +} + +fn run_bash_cmd(cfg: &Config, cmd: &String) -> Result<()> { + let url = cluster_url(cfg, &cfg.test_validator); + let exit = std::process::Command::new("bash") + .arg("-c") + .arg(cmd) + .env("ANCHOR_PROVIDER_URL", url) + .env("ANCHOR_WALLET", cfg.provider.wallet.to_string()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .output() + .unwrap(); + if !exit.status.success() { + std::process::exit(exit.status.code().unwrap_or(1)); + } + Ok(()) +} + fn login(_cfg_override: &ConfigOverride, token: String) -> Result<()> { let dir = shellexpand::tilde("~/.config/anchor"); if !Path::new(&dir.to_string()).exists() {