Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace.package]
version = "1.3.0"
version = "1.4.0"
edition = "2024"
authors = ["Axiom Team"]
license = "MIT"
Expand Down
68 changes: 17 additions & 51 deletions crates/cli/src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::io::{self, Write};

use axiom_sdk::{
AxiomSdk,
build::{BuildSdk, ConfigSource},
build::{BuildSdk, UploadExeArgs},
};
use clap::{Parser, Subcommand};
use comfy_table;
Expand All @@ -11,7 +11,10 @@ use eyre::Result;
use crate::{formatting::Formatter, progress::CliProgressCallback};

#[derive(Debug, Parser)]
#[command(name = "build", about = "Build the project on Axiom Proving Service")]
#[command(
name = "build",
about = "Upload a locally-built program (ELF + VMEXE) to the Axiom Proving Service"
)]
pub struct BuildCmd {
#[command(subcommand)]
command: Option<BuildSubcommand>,
Expand Down Expand Up @@ -53,60 +56,33 @@ enum BuildSubcommand {
#[clap(long, value_name = "TYPE", value_parser = ["exe", "elf", "source", "app_exe_commit", "all"])]
artifact: String,
},

/// Download build logs for a program
Logs {
/// The program ID to download logs for
#[clap(long, value_name = "ID")]
program_id: String,
},
}

#[derive(Debug, Parser)]
pub struct BuildArgs {
/// The configuration ID to use for the build
#[clap(long, value_name = "ID", conflicts_with = "config")]
/// The configuration ID to use (must reference an already-keyed config)
#[clap(long, value_name = "ID")]
config_id: Option<String>,

/// Path to an OpenVM TOML configuration file
#[clap(long, value_name = "PATH")]
config: Option<String>,

/// The binary to build, if there are multiple binaries in the project
/// The binary to upload, if the project has multiple binaries
#[clap(long, value_name = "BIN")]
bin: Option<String>,

/// Keep the tar archive after uploading
#[clap(long)]
keep_tarball: Option<bool>,

/// Comma-separated list of file patterns to exclude (e.g. "*.log,temp/*")
#[clap(long, value_name = "PATTERNS")]
exclude_files: Option<String>,

/// Comma-separated list of directories to include even if not tracked by git
#[clap(long, value_name = "DIRS")]
include_dirs: Option<String>,
/// Custom program name
#[clap(long, value_name = "NAME")]
program_name: Option<String>,

/// The project ID to associate with the build
/// The project ID to associate with the program
#[arg(long, value_name = "ID")]
project_id: Option<String>,

/// Run in detached mode (don't wait for completion)
#[clap(long)]
detach: bool,

/// Allow building with uncommitted changes
#[clap(long)]
allow_dirty: bool,

/// Specify default_num_gpus for this program
#[clap(long)]
default_num_gpus: Option<usize>,

/// OpenVM Rust toolchain version (e.g., nightly-2025-02-14)
#[clap(long, value_name = "VERSION")]
openvm_rust_toolchain: Option<String>,
}

impl BuildCmd {
Expand Down Expand Up @@ -168,14 +144,8 @@ impl BuildCmd {
program_id,
artifact,
}) => sdk.download_program(&program_id, &artifact),
Some(BuildSubcommand::Logs { program_id }) => sdk.download_build_logs(&program_id),
None => {
let program_dir = std::env::current_dir()?;
let config_source = match (self.build_args.config_id, self.build_args.config) {
(Some(config_id), _) => Some(ConfigSource::ConfigId(config_id)),
(_, Some(config)) => Some(ConfigSource::ConfigPath(config)),
(None, None) => None,
};

let project_id = {
let cache_path = program_dir.join(".axiom").join("project-id");
Expand Down Expand Up @@ -204,19 +174,15 @@ impl BuildCmd {
if name.is_empty() { None } else { Some(name) }
};

let args = axiom_sdk::build::BuildArgs {
config_source,
bin: self.build_args.bin,
keep_tarball: self.build_args.keep_tarball,
exclude_files: self.build_args.exclude_files,
include_dirs: self.build_args.include_dirs,
let args = UploadExeArgs {
config_id: self.build_args.config_id,
project_id,
project_name: project_name_for_creation.clone(),
allow_dirty: self.build_args.allow_dirty,
bin_name: self.build_args.bin,
program_name: self.build_args.program_name,
default_num_gpus: self.build_args.default_num_gpus,
openvm_rust_toolchain: self.build_args.openvm_rust_toolchain,
};
let program_id = sdk.register_new_program(&program_dir, args)?;
let program_id = sdk.upload_exe(&program_dir, args)?;

// Always fetch the latest build status to get project ID and print console URL
let status = sdk.get_build_status(&program_id)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ enum AxiomCommands {
Init(InitCmd),
/// Register Axiom API credentials
Register(RegisterCmd),
/// Build the project on Axiom Proving Service
/// Upload a locally-built program (ELF + VMEXE) to the Axiom Proving Service
Build(BuildCmd),
/// Generate a proof using the Axiom Proving Service
Prove(ProveCmd),
Expand Down
152 changes: 58 additions & 94 deletions crates/sdk/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ pub trait BuildSdk {
fn get_app_exe_commit(&self, program_id: &str) -> Result<Vec<u8>>;

fn download_program(&self, program_id: &str, program_type: &str) -> Result<()>;
fn download_build_logs(&self, program_id: &str) -> Result<()>;
fn register_new_program(
&self,
program_dir: impl AsRef<Path>,
Expand Down Expand Up @@ -357,28 +356,6 @@ impl BuildSdk for AxiomSdk {
}
}

fn download_build_logs(&self, program_id: &str) -> Result<()> {
let url = format!("{}/programs/{}/logs", self.config.api_url, program_id);
let build_dir = std::path::PathBuf::from("axiom-artifacts")
.join(format!("program-{}", program_id))
.join("artifacts");
std::fs::create_dir_all(&build_dir).context(format!(
"Failed to create build directory: {}",
build_dir.display()
))?;

let filename = build_dir.join("logs.txt");
let response = authenticated_get(&self.config, &url)?;
crate::download_file_streaming(
response,
filename.clone(),
"Failed to download build logs",
)?;
self.callback
.on_success(&format!("✓ {}", filename.display()));
Ok(())
}

fn register_new_program(
&self,
program_dir: impl AsRef<Path>,
Expand Down Expand Up @@ -839,93 +816,84 @@ impl AxiomSdk {
eyre::bail!("Not in a Rust project. Make sure Cargo.toml exists.");
}

// Look for the openvm build output directory
let target_dir = program_dir.as_ref().join("target").join("openvm");
if !target_dir.exists() {
// openvm v2 `cargo openvm build` output layout (run from the guest crate dir):
// - transpiled VMEXE: <dir>/openvm/<profile>/<bin>.vmexe (SIBLING of target/)
// - raw ELF: <dir>/target/<openvm-target>/<profile>/<bin> (no extension)
// NOTE: assumes the default target dir; custom CARGO_TARGET_DIR / workspace target
// dirs are not handled yet.
let profile = "release";
let vmexe_dir = program_dir.as_ref().join("openvm").join(profile);
if !vmexe_dir.exists() {
eyre::bail!(
"OpenVM build output not found. Please run 'cargo openvm build' first.\nExpected directory: {}",
target_dir.display()
);
}

// Find the release directory
// TODO: Support other profiles beyond release (e.g., dev, custom profiles)
let release_dir = target_dir.join("release");
if !release_dir.exists() {
eyre::bail!(
"OpenVM release build not found. Please run 'cargo openvm build' first.\nExpected directory: {}",
release_dir.display()
vmexe_dir.display()
);
}

// Find ELF and VMEXE files
let elf_files: Vec<_> = std::fs::read_dir(&release_dir)?
// Find the transpiled VMEXE files.
let vmexe_files: Vec<_> = std::fs::read_dir(&vmexe_dir)?
.filter_map(Result::ok)
.filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("elf"))
.filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("vmexe"))
.collect();

if elf_files.is_empty() {
if vmexe_files.is_empty() {
eyre::bail!(
"No ELF files found in {}. Please run 'cargo openvm build' first.",
release_dir.display()
"No VMEXE files found in {}. Please run 'cargo openvm build' first.",
vmexe_dir.display()
);
}

// Filter by bin_name if provided and there are multiple ELFs
let elf_path = if elf_files.len() > 1 {
if let Some(bin_name) = &args.bin_name {
// Filter ELF files by bin_name
let matching_elf = elf_files.iter().find(|entry| {
entry
.path()
let available_bins = |files: &[std::fs::DirEntry]| -> Vec<String> {
files
.iter()
.filter_map(|e| {
e.path()
.file_stem()
.and_then(|s| s.to_str())
.map(|name| name == bin_name)
.unwrap_or(false)
});

if let Some(elf_entry) = matching_elf {
elf_entry.path()
} else {
let available: Vec<_> = elf_files
.iter()
.filter_map(|e| {
e.path()
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
})
.collect();
eyre::bail!(
"ELF file '{}' not found. Available ELF files: {}",
.map(|s| s.to_string())
})
.collect()
};

// Select the VMEXE (by --bin-name when the project has multiple binaries).
let vmexe_path = if vmexe_files.len() > 1 {
if let Some(bin_name) = &args.bin_name {
match vmexe_files.iter().find(|entry| {
entry.path().file_stem().and_then(|s| s.to_str()) == Some(bin_name.as_str())
}) {
Some(entry) => entry.path(),
None => eyre::bail!(
"VMEXE for bin '{}' not found. Available: {}",
bin_name,
available.join(", ")
);
available_bins(&vmexe_files).join(", ")
),
}
} else {
let available: Vec<_> = elf_files
.iter()
.filter_map(|e| {
e.path()
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
})
.collect();
eyre::bail!(
"Multiple ELF files found. Please specify which one to upload using --bin-name. Available: {}",
available.join(", ")
"Multiple binaries found. Specify which one with --bin-name. Available: {}",
available_bins(&vmexe_files).join(", ")
);
}
} else {
elf_files[0].path()
vmexe_files[0].path()
};
let vmexe_path = elf_path.with_extension("vmexe");

if !vmexe_path.exists() {
// Derive the binary name from the VMEXE, then locate the matching raw ELF.
let bin = vmexe_path
.file_stem()
.and_then(|s| s.to_str())
.ok_or_eyre("Could not determine binary name from VMEXE path")?;
let elf_path = program_dir
.as_ref()
.join("target")
.join(OPENVM_RUSTC_TARGET)
.join(profile)
.join(bin);

if !elf_path.exists() {
eyre::bail!(
"VMEXE file not found at {}. Please run 'cargo openvm build' first.",
vmexe_path.display()
"ELF file not found at {}. Please run 'cargo openvm build' first.",
elf_path.display()
);
}

Expand Down Expand Up @@ -962,11 +930,10 @@ impl AxiomSdk {
.ok_or_eyre("No config_id provided and no default config_id in ~/.axiom/config.json")?;
callback.on_field("Config ID", config_id);

// Build URL with query parameters
let mut url = format!(
"{}/programs/upload-exe?config_id={}",
self.config.api_url, config_id
);
// Build URL with query parameters. API 2.0 unified registration onto POST /programs
// (the /programs/upload-exe endpoint and the source-tarball build path were removed),
// and dropped the server-side bin_name param (the binary is chosen at build time).
let mut url = format!("{}/programs?config_id={}", self.config.api_url, config_id);

if let Some(project_id) = &args.project_id {
url.push_str(&format!("&project_id={}", project_id));
Expand All @@ -976,9 +943,6 @@ impl AxiomSdk {
url::form_urlencoded::byte_serialize(project_name.as_bytes()).collect();
url.push_str(&format!("&project_name={}", encoded));
}
if let Some(bin_name) = &args.bin_name {
url.push_str(&format!("&bin_name={}", bin_name));
}
if let Some(program_name) = &args.program_name {
let encoded: String =
url::form_urlencoded::byte_serialize(program_name.as_bytes()).collect();
Expand Down
Loading