Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions cli/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{get_keypair, is_hidden, keys_sync, DEFAULT_RPC_PORT};
use crate::{get_keypair, is_hidden, keys_sync, target_dir, DEFAULT_RPC_PORT};
use anchor_client::Cluster;
use anchor_lang_idl::types::Idl;
use anyhow::{anyhow, bail, Context, Error, Result};
Expand Down Expand Up @@ -219,7 +219,7 @@ impl WithPath<Config> {
let cargo = Manifest::from_path(path.join("Cargo.toml"))?;
let lib_name = cargo.lib_name()?;

let idl_filepath = Path::new("target")
let idl_filepath = target_dir()
.join("idl")
.join(&lib_name)
.with_extension("json");
Expand Down Expand Up @@ -531,7 +531,7 @@ impl Config {
if filename.to_str() == Some("Anchor.toml") {
// Make sure the program id is correct (only on the initial build)
let mut cfg = Config::from_path(&p)?;
let deploy_dir = p.parent().unwrap().join("target").join("deploy");
let deploy_dir = target_dir().join("deploy");
if !deploy_dir.exists() && !cfg.programs.contains_key(&Cluster::Localnet) {
println!("Updating program ids...");
fs::create_dir_all(deploy_dir)?;
Expand Down Expand Up @@ -1430,7 +1430,7 @@ impl Program {

// Lazily initializes the keypair file with a new key if it doesn't exist.
pub fn keypair_file(&self) -> Result<WithPath<File>> {
let deploy_dir_path = Path::new("target").join("deploy");
let deploy_dir_path = target_dir().join("deploy");
fs::create_dir_all(&deploy_dir_path)
.with_context(|| format!("Error creating directory with path: {deploy_dir_path:?}"))?;
let path = std::env::current_dir()
Expand All @@ -1451,7 +1451,7 @@ impl Program {
}

pub fn binary_path(&self, verifiable: bool) -> PathBuf {
let path = Path::new("target")
let path = target_dir()
.join(if verifiable { "verifiable" } else { "deploy" })
.join(&self.lib_name)
.with_extension("so");
Expand Down
66 changes: 52 additions & 14 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToSnakeCase};
use regex::{Regex, RegexBuilder};
use rust_template::{ProgramTemplate, TestTemplate};
use semver::{Version, VersionReq};
use serde::Deserialize;
use serde_json::{json, Map, Value as JsonValue};
use solana_cli_config::Config as SolanaCliConfig;
use solana_commitment_config::CommitmentConfig;
Expand Down Expand Up @@ -1378,7 +1379,7 @@ fn init(
cfg.toolchain.package_manager = Some(package_manager);

let mut localnet = BTreeMap::new();
let program_id = rust_template::get_or_create_program_id(&rust_name);
let program_id = rust_template::get_or_create_program_id(&rust_name, "target");
localnet.insert(
rust_name,
ProgramDeployment {
Expand Down Expand Up @@ -1518,7 +1519,7 @@ fn new(
programs.insert(
name.clone(),
ProgramDeployment {
address: rust_template::get_or_create_program_id(&name),
address: rust_template::get_or_create_program_id(&name, target_dir()),
path: None,
idl: None,
},
Expand Down Expand Up @@ -1733,13 +1734,13 @@ pub fn build(

let idl_out = match idl {
Some(idl) => Some(PathBuf::from(idl)),
None => Some(cfg_parent.join("target").join("idl")),
None => Some(target_dir().join("idl")),
};
fs::create_dir_all(idl_out.as_ref().unwrap())?;

let idl_ts_out = match idl_ts {
Some(idl_ts) => Some(PathBuf::from(idl_ts)),
None => Some(cfg_parent.join("target").join("types")),
None => Some(target_dir().join("types")),
};
fs::create_dir_all(idl_ts_out.as_ref().unwrap())?;

Expand Down Expand Up @@ -1906,7 +1907,7 @@ fn build_cwd_verifiable(
) -> Result<()> {
// Create output dirs.
let workspace_dir = cfg.path().parent().unwrap().canonicalize()?;
let target_dir = workspace_dir.join("target");
let target_dir = target_dir();
fs::create_dir_all(target_dir.join("verifiable"))?;
fs::create_dir_all(target_dir.join("idl"))?;
fs::create_dir_all(target_dir.join("types"))?;
Expand Down Expand Up @@ -1938,17 +1939,15 @@ fn build_cwd_verifiable(
let idl = generate_idl(cfg, skip_lint, no_docs, &cargo_args)?;
// Write out the JSON file.
println!("Writing the IDL file");
let out_file = workspace_dir
.join("target")
let out_file = target_dir
.join("idl")
.join(&idl.metadata.name)
.with_extension("json");
write_idl(&idl, OutFile::File(out_file))?;

// Write out the TypeScript type.
println!("Writing the .ts file");
let ts_file = workspace_dir
.join("target")
let ts_file = target_dir
.join("types")
.join(&idl.metadata.name)
.with_extension("ts");
Expand Down Expand Up @@ -3403,7 +3402,7 @@ fn validator_flags(
idl.address = address;

// Persist it.
let idl_out = Path::new("target")
let idl_out = target_dir()
.join("idl")
.join(&idl.metadata.name)
.with_extension("json");
Expand Down Expand Up @@ -3763,7 +3762,7 @@ fn stream_solana_logs(config: &WithPath<Config>, rpc_url: &str) -> Result<Vec<Lo

// Subscribe to logs for all workspace programs
for program in config.read_all_programs()? {
let idl_path = Path::new("target")
let idl_path = target_dir()
.join("idl")
.join(&program.lib_name)
.with_extension("json");
Expand Down Expand Up @@ -4082,7 +4081,7 @@ fn clean(cfg_override: &ConfigOverride) -> Result<()> {
};

let dot_anchor_dir = workspace_root.join(".anchor");
let target_dir = workspace_root.join("target");
let target_dir = crate::target_dir();
let deploy_dir = target_dir.join("deploy");

if dot_anchor_dir.exists() {
Expand Down Expand Up @@ -4782,9 +4781,48 @@ fn localnet(
})?
}

/// Return the cargo build artifacts directory. Caches the result assuming that
/// a single run will only work with a single rust workspace. Exits the process
/// if the directory cannot be determined.
pub fn target_dir() -> PathBuf {
static TARGET_DIR: LazyLock<Result<PathBuf>> = LazyLock::new(target_dir_no_cache);
match TARGET_DIR.as_ref() {
Ok(path) => path.clone(),
Err(e) => {
eprintln!("Error: {e:?}");
std::process::exit(1);
}
}
}

/// Return the cargo build artifacts directory.
fn target_dir_no_cache() -> Result<PathBuf> {
// `cargo metadata` produces a JSON blob from which we extract the
// `target_directory` field.
let output = std::process::Command::new("cargo")
.args(["metadata", "--no-deps", "--format-version=1"])
.output()
.context("Failed to execute 'cargo metadata'")?;

if !output.status.success() {
let stderr_msg = String::from_utf8_lossy(&output.stderr);
bail!("'cargo metadata' failed with: {stderr_msg}");
}
Comment on lines +4799 to +4810
Copy link
Copy Markdown
Collaborator

@jamie-osec jamie-osec Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second review, I think this is a confusing design with 4 distinct results

  • If the cargo execution fails, we return an error
  • If cargo execution succeeds, but the status is not success
    • We use a heuristic to special case the error from the anchor init case
    • Otherwise, we print an error and hard exit immediately
  • Otherwise, the output is parsed and used

I would suggest

  • Use Path::from("target") directly as previously in anchor init as we know special handling is required, so calling this is pointless
  • Either always exit, or always return an error from this function
    • I would probably suggest the former, as there's not much that can be done to handle this error, and it simplifies the API for callers

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jamie-osec: I implemented both suggestions.

The anchor init handling to simplify the responsibilities of target_dir() is in this commit: dimalinux@27d6739

Having target_dir() exit the process in all failure scenarios is in this commit:
dimalinux@3bc3a0e


#[derive(Deserialize)]
struct CargoMetadata {
target_directory: PathBuf,
}

let metadata: CargoMetadata = serde_json::from_slice(&output.stdout)
.context("Failed to parse 'cargo metadata' output")?;

Ok(metadata.target_directory)
}

// with_workspace ensures the current working directory is always the top level
// workspace directory, i.e., where the `Anchor.toml` file is located, before
// and after the closure invocation.
// where the `Anchor.toml` file is located, before and after the closure
// invocation.
//
// The closure passed into this function must never change the working directory
// to be outside the workspace. Doing so will have undefined behavior.
Expand Down
20 changes: 12 additions & 8 deletions cli/src/rust_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub enum ProgramTemplate {
/// Create a program from the given name and template.
pub fn create_program(name: &str, template: ProgramTemplate, with_mollusk: bool) -> Result<()> {
let program_path = Path::new("programs").join(name);
let target_path = Path::new("target");
let common_files = vec![
("Cargo.toml".into(), workspace_manifest().into()),
("rust-toolchain.toml".into(), rust_toolchain_toml()),
Expand All @@ -44,9 +45,11 @@ pub fn create_program(name: &str, template: ProgramTemplate, with_mollusk: bool)
let template_files = match template {
ProgramTemplate::Single => {
println!("Note: Using single-file template. For better code organization and maintainability, consider using --template multiple (default).");
create_program_template_single(name, &program_path)
create_program_template_single(name, &program_path, target_path)
}
ProgramTemplate::Multiple => {
create_program_template_multiple(name, &program_path, target_path)
}
ProgramTemplate::Multiple => create_program_template_multiple(name, &program_path),
};

create_files(&[common_files, template_files].concat())
Expand All @@ -64,7 +67,7 @@ profile = "minimal"
}

/// Create a program with a single `lib.rs` file.
fn create_program_template_single(name: &str, program_path: &Path) -> Files {
fn create_program_template_single(name: &str, program_path: &Path, target_path: &Path) -> Files {
vec![(
program_path.join("src").join("lib.rs"),
format!(
Expand All @@ -85,14 +88,14 @@ pub mod {} {{
#[derive(Accounts)]
pub struct Initialize {{}}
"#,
get_or_create_program_id(name),
get_or_create_program_id(name, target_path),
name.to_snake_case(),
),
)]
}

/// Create a program with multiple files for instructions, state...
fn create_program_template_multiple(name: &str, program_path: &Path) -> Files {
fn create_program_template_multiple(name: &str, program_path: &Path, target_path: &Path) -> Files {
let src_path = program_path.join("src");
vec![
(
Expand Down Expand Up @@ -120,7 +123,7 @@ pub mod {} {{
}}
}}
"#,
get_or_create_program_id(name),
get_or_create_program_id(name, target_path),
name.to_snake_case(),
),
),
Expand Down Expand Up @@ -239,8 +242,9 @@ unexpected_cfgs = {{ level = "warn", check-cfg = ['cfg(target_os, values("solana
}

/// 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")
pub fn get_or_create_program_id(name: &str, target_path: impl AsRef<Path>) -> Pubkey {
let keypair_path = target_path
.as_ref()
.join("deploy")
.join(format!("{}-keypair.json", name.to_snake_case()));

Expand Down
7 changes: 6 additions & 1 deletion tests/bench/tests/binary-size.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { execSync } from "child_process";
import * as fs from "fs/promises";
import path from "path";

Expand All @@ -9,8 +10,12 @@ describe("Binary size", () => {
const binarySize: BinarySize = {};

it("Measure binary size", async () => {
const output = execSync("cargo metadata --no-deps --format-version=1", {
encoding: "utf8",
});
const metadata = JSON.parse(output);
const stat = await fs.stat(
path.join("target", "deploy", `${IDL.metadata.name}.so`)
path.join(metadata.target_directory, "deploy", `${IDL.metadata.name}.so`)
);
binarySize[IDL.metadata.name] = stat.size;
});
Expand Down
19 changes: 18 additions & 1 deletion ts/packages/anchor/src/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as toml from "toml";
import camelcase from "camelcase";
import { execSync } from "child_process";
import { Program } from "./program/index.js";
import { isBrowser } from "./utils/common.js";
import { Idl } from "./idl.js";
Expand Down Expand Up @@ -54,7 +55,23 @@ const workspace = new Proxy(
//
// To avoid the above problem with numbers, read the `idl` directory and
// compare the camelCased version of both file names and `programName`.
const idlDirPath = path.join("target", "idl");
let metadata: { target_directory: string };
try {
const output = execSync(
"cargo metadata --no-deps --format-version=1",
{
encoding: "utf8",
}
);
metadata = JSON.parse(output);
} catch (err) {
throw new Error(
`Failed to run 'cargo metadata'. Ensure Rust and Cargo are installed and the project is valid.\nOriginal error: ${
err instanceof Error ? err.message : err
}`
);
}
const idlDirPath = path.join(metadata.target_directory, "idl");
const fileName = fs
.readdirSync(idlDirPath)
.find((name) => camelcase(path.parse(name).name) === programName);
Expand Down