Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
11 changes: 6 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 @@ -218,7 +218,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 @@ -551,7 +551,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 @@ -1363,7 +1363,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 @@ -1384,7 +1384,8 @@ impl Program {
}

pub fn binary_path(&self, verifiable: bool) -> PathBuf {
let path = Path::new("target")
let path = target_dir()
.expect("Unable to determine `target` dir")
.join(if verifiable { "verifiable" } else { "deploy" })
.join(&self.lib_name)
.with_extension("so");
Expand Down
65 changes: 53 additions & 12 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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 @@ -1731,13 +1732,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 @@ -1912,7 +1913,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 @@ -1945,17 +1946,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 @@ -3436,7 +3435,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 @@ -3655,7 +3654,7 @@ fn stream_logs(config: &WithPath<Config>, rpc_url: &str) -> Result<Vec<LogStream

// 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 @@ -3890,7 +3889,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 @@ -4586,9 +4585,51 @@ fn localnet(
})?
}

/// Return the directory where cargo is storing build artifacts. Caches the
/// result assuming that a single run will only work with a single rust
/// workspace.
pub fn target_dir() -> Result<PathBuf> {
static TARGET_DIR: LazyLock<Result<PathBuf>> = LazyLock::new(target_dir_no_cache);
match &*TARGET_DIR {
Ok(path) => Ok(path.clone()),
Err(e) => Err(anyhow::anyhow!(e.to_string())),
}
}

/// Return the directory where cargo is storing build artifacts.
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);
if stderr_msg.contains("Cargo.toml") {
// `anchor init` starts populating the cargo artifacts dir
// before creating `Cargo.toml`, in which case "target" in
// the current dir is the desired behavior.
return Ok(PathBuf::from("target"));
}
eprintln!("'cargo metadata' failed with: {stderr_msg}");
std::process::exit(output.status.code().unwrap_or(1));
}
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)?;

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
7 changes: 4 additions & 3 deletions cli/src/rust_template.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
config::ProgramWorkspace, create_files, override_or_create_files, Files, PackageManager,
VERSION,
config::ProgramWorkspace, create_files, override_or_create_files, target_dir, Files,
PackageManager, VERSION,
};
use anyhow::Result;
use clap::{Parser, ValueEnum};
Expand Down Expand Up @@ -240,7 +240,8 @@ 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")
let keypair_path = target_dir()
.expect("Unable to determine `target` dir")
.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
7 changes: 6 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,11 @@ 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");
const output = execSync("cargo metadata --no-deps --format-version 1", {
encoding: "utf8",
});
const metadata = JSON.parse(output);
const idlDirPath = path.join(metadata.target_directory, "idl");
const fileName = fs
.readdirSync(idlDirPath)
.find((name) => camelcase(path.parse(name).name) === programName);
Expand Down