diff --git a/src/build/mod.rs b/src/build/mod.rs index d56ce32c..71484ade 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -36,6 +36,9 @@ mod caller_utils_generator; mod caller_utils_ts_generator; mod wit_generator; +// Default Rust toolchain to use for builds +pub const DEFAULT_RUST_TOOLCHAIN: &str = "+1.85.1"; + const PY_VENV_NAME: &str = "process_env"; const JAVASCRIPT_SRC_PATH: &str = "src/lib.js"; const PYTHON_SRC_PATH: &str = "src/lib.py"; @@ -942,6 +945,7 @@ async fn compile_rust_wasm_process( process_dir: &Path, features: &str, verbose: bool, + toolchain: &str, ) -> Result<()> { let Some(package_dir) = process_dir.parent() else { return Err(eyre!( @@ -981,7 +985,7 @@ async fn compile_rust_wasm_process( // Build the module using Cargo let mut args = vec![ - "+stable", + toolchain, "build", "-p", &process_name, @@ -1148,9 +1152,10 @@ async fn compile_package_item( is_py_process: bool, is_js_process: bool, verbose: bool, + toolchain: String, ) -> Result<()> { if is_rust_process { - compile_rust_wasm_process(&path, &features, verbose).await?; + compile_rust_wasm_process(&path, &features, verbose, &toolchain).await?; } else if is_py_process { let python = get_python_version(None, None)? .ok_or_else(|| eyre!("kit requires Python 3.10 or newer"))?; @@ -1209,6 +1214,7 @@ async fn fetch_dependencies( hyperapp: bool, force: bool, verbose: bool, + toolchain: &str, ) -> Result<()> { if let Err(e) = Box::pin(execute( package_dir, @@ -1229,6 +1235,7 @@ async fn fetch_dependencies( force, verbose, true, + toolchain, )) .await { @@ -1267,6 +1274,7 @@ async fn fetch_dependencies( force, verbose, false, + toolchain, )) .await?; fetch_local_built_dependency(apis, wasm_paths, &local_dependency)?; @@ -1453,6 +1461,7 @@ async fn check_and_populate_dependencies( metadata: &Erc721Metadata, skip_deps_check: bool, verbose: bool, + toolchain: &str, ) -> Result<(HashMap>, HashSet)> { let mut checked_rust = false; let mut checked_py = false; @@ -1468,15 +1477,15 @@ async fn check_and_populate_dependencies( let path = entry.path(); if path.is_dir() { if path.join(RUST_SRC_PATH).exists() && !checked_rust && !skip_deps_check { - let deps = check_rust_deps()?; - get_deps(deps, &mut recv_kill, false, verbose).await?; + let deps = check_rust_deps(toolchain)?; + get_deps(deps, &mut recv_kill, false, verbose, toolchain).await?; checked_rust = true; } else if path.join(PYTHON_SRC_PATH).exists() && !checked_py { check_py_deps()?; checked_py = true; } else if path.join(JAVASCRIPT_SRC_PATH).exists() && !checked_js && !skip_deps_check { let deps = check_js_deps()?; - get_deps(deps, &mut recv_kill, false, verbose).await?; + get_deps(deps, &mut recv_kill, false, verbose, toolchain).await?; checked_js = true; } else if Some("api") == path.file_name().and_then(|s| s.to_str()) { // read api files: to be used in build @@ -1583,11 +1592,18 @@ async fn compile_package( verbose: bool, hyperapp_processed_projects: Option>, ignore_deps: bool, // for internal use; may cause problems when adding recursive deps + toolchain: &str, ) -> Result<()> { let metadata = read_and_update_metadata(package_dir)?; let mut wasm_paths = HashSet::new(); - let (mut apis, dependencies) = - check_and_populate_dependencies(package_dir, &metadata, skip_deps_check, verbose).await?; + let (mut apis, dependencies) = check_and_populate_dependencies( + package_dir, + &metadata, + skip_deps_check, + verbose, + toolchain, + ) + .await?; info!("dependencies: {dependencies:?}"); if !ignore_deps && !dependencies.is_empty() { @@ -1608,6 +1624,7 @@ async fn compile_package( hyperapp, force, verbose, + toolchain, ) .await? } @@ -1664,6 +1681,7 @@ async fn compile_package( is_py_process, is_js_process, verbose.clone(), + toolchain.to_string(), )); } while let Some(res) = tasks.join_next().await { @@ -1747,6 +1765,7 @@ pub async fn execute( force: bool, verbose: bool, ignore_deps: bool, // for internal use; may cause problems when adding recursive deps + toolchain: &str, ) -> Result<()> { debug!( "execute: @@ -1858,7 +1877,7 @@ pub async fn execute( if !skip_deps_check { let mut recv_kill = make_fake_kill_chan(); let deps = check_js_deps()?; - get_deps(deps, &mut recv_kill, false, verbose).await?; + get_deps(deps, &mut recv_kill, false, verbose, DEFAULT_RUST_TOOLCHAIN).await?; } let valid_node = get_newest_valid_node_version(None, None)?; for ui_dir in ui_dirs { @@ -1884,6 +1903,7 @@ pub async fn execute( verbose, hyperapp_processed_projects, ignore_deps, + toolchain, ) .await?; } diff --git a/src/build_start_package/mod.rs b/src/build_start_package/mod.rs index 6e5469ff..7a9c2f21 100644 --- a/src/build_start_package/mod.rs +++ b/src/build_start_package/mod.rs @@ -26,6 +26,7 @@ pub async fn execute( reproducible: bool, force: bool, verbose: bool, + toolchain: &str, ) -> Result<()> { build::execute( package_dir, @@ -46,6 +47,7 @@ pub async fn execute( force, verbose, false, + toolchain, ) .await?; start_package::execute(package_dir, url).await?; diff --git a/src/chain/mod.rs b/src/chain/mod.rs index ce55240a..5a40a97f 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -8,6 +8,7 @@ use reqwest::Client; use tokio::time::{sleep, Duration}; use tracing::{debug, info, instrument}; +use crate::build; use crate::run_tests::cleanup::{clean_process_by_pid, cleanup_on_signal}; use crate::run_tests::types::BroadcastRecvBool; use crate::setup::{check_foundry_deps, get_deps}; @@ -257,7 +258,14 @@ pub async fn start_chain( tracing: bool, ) -> Result> { let deps = check_foundry_deps()?; - get_deps(deps, &mut recv_kill, false, verbose).await?; + get_deps( + deps, + &mut recv_kill, + false, + verbose, + build::DEFAULT_RUST_TOOLCHAIN, + ) + .await?; info!("Checking for Anvil on port {}...", port); if wait_for_anvil(port, 1, None).await.is_ok() { diff --git a/src/dev_ui/mod.rs b/src/dev_ui/mod.rs index 5d2cbe44..11e86a74 100644 --- a/src/dev_ui/mod.rs +++ b/src/dev_ui/mod.rs @@ -4,7 +4,7 @@ use std::process::Command; use color_eyre::{eyre::eyre, Result}; use tracing::{info, instrument}; -use crate::build::{make_fake_kill_chan, run_command}; +use crate::build::{make_fake_kill_chan, run_command, DEFAULT_RUST_TOOLCHAIN}; use crate::setup::{check_js_deps, get_deps, get_newest_valid_node_version}; #[instrument(level = "trace", skip_all)] @@ -17,7 +17,7 @@ pub async fn execute( if !skip_deps_check { let deps = check_js_deps()?; let mut recv_kill = make_fake_kill_chan(); - get_deps(deps, &mut recv_kill, false, false).await?; + get_deps(deps, &mut recv_kill, false, false, DEFAULT_RUST_TOOLCHAIN).await?; } let valid_node = get_newest_valid_node_version(None, None)?; diff --git a/src/main.rs b/src/main.rs index 6d363f75..041525ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,6 +53,31 @@ fn parse_u128_with_underscores(s: &str) -> Result { .map_err(|_| "Invalid number format") } +#[instrument(level = "trace", skip_all)] +fn parse_rust_toolchain(s: &str) -> Result { + // Validate the format: must start with '+' followed by version or channel name + if !s.starts_with('+') { + return Err("Rust toolchain must start with '+' (e.g., '+stable', '+1.85.1', '+nightly')"); + } + + let toolchain = &s[1..]; + + // Check if it's a valid channel name or version format + if toolchain.is_empty() { + return Err("Rust toolchain cannot be empty after '+'"); + } + + // Basic validation: alphanumeric, dots, dashes, and hyphens are allowed + if !toolchain + .chars() + .all(|c| c.is_alphanumeric() || c == '.' || c == '-' || c == '_') + { + return Err("Invalid characters in Rust toolchain specification"); + } + + Ok(s.to_string()) +} + #[instrument(level = "trace", skip_all)] async fn get_latest_commit_sha_from_branch( owner: &str, @@ -247,6 +272,7 @@ async fn execute( let reproducible = matches.get_one::("REPRODUCIBLE").unwrap(); let force = matches.get_one::("FORCE").unwrap(); let verbose = matches.get_one::("VERBOSE").unwrap(); + let toolchain = matches.get_one::("TOOLCHAIN").unwrap(); build::execute( &package_dir, @@ -267,6 +293,7 @@ async fn execute( *force, *verbose, false, + toolchain, ) .await } @@ -312,6 +339,7 @@ async fn execute( let reproducible = matches.get_one::("REPRODUCIBLE").unwrap(); let force = matches.get_one::("FORCE").unwrap(); let verbose = matches.get_one::("VERBOSE").unwrap(); + let toolchain = matches.get_one::("TOOLCHAIN").unwrap(); build_start_package::execute( &package_dir, @@ -331,6 +359,7 @@ async fn execute( *reproducible, *force, *verbose, + toolchain, ) .await } @@ -473,6 +502,7 @@ async fn execute( let foundry_optional = matches.get_one::("FOUNDRY_OPTIONAL").unwrap(); let javascript_optional = matches.get_one::("JAVASCRIPT_OPTIONAL").unwrap(); let non_interactive = matches.get_one::("NON_INTERACTIVE").unwrap(); + let toolchain = matches.get_one::("TOOLCHAIN").unwrap(); let mut recv_kill = build::make_fake_kill_chan(); setup::execute( @@ -483,6 +513,7 @@ async fn execute( *javascript_optional, *non_interactive, *verbose, + toolchain, ) .await } @@ -829,6 +860,14 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .help("If set, output stdout and stderr") .required(false) ) + .arg(Arg::new("TOOLCHAIN") + .action(ArgAction::Set) + .long("toolchain") + .help("Rust toolchain to use (e.g., '+stable', '+1.85.1', '+nightly')") + .default_value(build::DEFAULT_RUST_TOOLCHAIN) + .value_parser(clap::builder::ValueParser::new(parse_rust_toolchain)) + .required(false) + ) ) .subcommand(Command::new("build-start-package") .about("Build and start a Hyperware package") @@ -942,6 +981,14 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .help("If set, output stdout and stderr") .required(false) ) + .arg(Arg::new("TOOLCHAIN") + .action(ArgAction::Set) + .long("toolchain") + .help("Rust toolchain to use (e.g., '+stable', '+1.85.1', '+nightly')") + .default_value(build::DEFAULT_RUST_TOOLCHAIN) + .value_parser(clap::builder::ValueParser::new(parse_rust_toolchain)) + .required(false) + ) ) .subcommand(Command::new("chain") .about("Start a local chain for development") @@ -1297,6 +1344,14 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .help("If set, do not prompt and instead always reply `Y` to prompts (i.e. automatically install dependencies without user input)") .required(false) ) + .arg(Arg::new("TOOLCHAIN") + .action(ArgAction::Set) + .long("toolchain") + .help("Rust toolchain to use (e.g., '+stable', '+1.85.1', '+nightly')") + .default_value(build::DEFAULT_RUST_TOOLCHAIN) + .value_parser(clap::builder::ValueParser::new(parse_rust_toolchain)) + .required(false) + ) ) .subcommand(Command::new("start-package") .about("Start a built Hyprware package") diff --git a/src/run_tests/mod.rs b/src/run_tests/mod.rs index 6e86b664..f9f78828 100644 --- a/src/run_tests/mod.rs +++ b/src/run_tests/mod.rs @@ -13,7 +13,7 @@ use tracing::{debug, info, instrument}; use hyperware_process_lib::kernel_types::PackageManifestEntry; use crate::boot_fake_node; -use crate::build; +use crate::build::{self, DEFAULT_RUST_TOOLCHAIN}; use crate::chain; use crate::inject_message; use crate::start_package; @@ -382,6 +382,7 @@ async fn build_packages( false, false, false, + DEFAULT_RUST_TOOLCHAIN, ) .await?; debug!("Start {path:?}"); @@ -408,6 +409,7 @@ async fn build_packages( false, false, false, + DEFAULT_RUST_TOOLCHAIN, ) .await?; } @@ -431,6 +433,7 @@ async fn build_packages( false, false, false, + DEFAULT_RUST_TOOLCHAIN, ) .await?; } diff --git a/src/setup/mod.rs b/src/setup/mod.rs index db09cd25..6d948c67 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -209,20 +209,20 @@ fn call_with_nvm(arg: &str, verbose: bool) -> Result<()> { } #[instrument(level = "trace", skip_all)] -fn call_rustup(arg: &str, verbose: bool) -> Result<()> { +fn call_rustup(arg: &str, verbose: bool, toolchain: &str) -> Result<()> { run_command( - Command::new("bash").args(&["-c", &format!("rustup +stable {}", arg)]), + Command::new("bash").args(&["-c", &format!("rustup {} {}", toolchain, arg)]), verbose, )?; Ok(()) } #[instrument(level = "trace", skip_all)] -fn call_cargo(arg: &str, verbose: bool) -> Result<()> { +fn call_cargo(arg: &str, verbose: bool, toolchain: &str) -> Result<()> { let command = if arg.contains("--color=always") { - format!("cargo +stable {}", arg) + format!("cargo {} {}", toolchain, arg) } else { - format!("cargo +stable --color=always {}", arg) + format!("cargo {} --color=always {}", toolchain, arg) }; run_command(Command::new("bash").args(&["-c", &command]), verbose)?; Ok(()) @@ -252,11 +252,11 @@ fn parse_version(version_str: &str) -> Option<(u32, u32)> { } #[instrument(level = "trace", skip_all)] -fn check_rust_toolchains_targets() -> Result> { +fn check_rust_toolchains_targets(toolchain: &str) -> Result> { let mut missing_deps = Vec::new(); let output = Command::new("rustup") - .arg("+stable") + .arg(toolchain) .arg("show") .output()? .stdout; @@ -367,7 +367,7 @@ fn install_foundry(verbose: bool) -> Result<()> { /// Check for Rust deps, returning a Vec of not found: can be automatically fetched #[instrument(level = "trace", skip_all)] -pub fn check_rust_deps() -> Result> { +pub fn check_rust_deps(toolchain: &str) -> Result> { if !is_command_installed("rustup")? { // don't have rust -> missing all return Ok(vec![ @@ -377,7 +377,7 @@ pub fn check_rust_deps() -> Result> { ]); } - let mut missing_deps = check_rust_toolchains_targets()?; + let mut missing_deps = check_rust_toolchains_targets(toolchain)?; if !is_command_installed("wasm-tools")? { missing_deps.push(Dependency::WasmTools); } @@ -406,13 +406,14 @@ pub async fn get_deps( recv_kill: &mut BroadcastRecvBool, non_interactive: bool, verbose: bool, + toolchain: &str, ) -> Result<()> { if deps.is_empty() { return Ok(()); } if non_interactive { - install_deps(deps, verbose)?; + install_deps(deps, verbose, toolchain)?; } else { // If setup required, request user permission print!( @@ -454,7 +455,7 @@ pub async fn get_deps( }; let response = response.trim().to_lowercase(); match response.as_str() { - "y" | "yes" | "" => install_deps(deps, verbose)?, + "y" | "yes" | "" => install_deps(deps, verbose, toolchain)?, r => warn!("Got '{}'; not getting deps.", r), } } @@ -462,7 +463,7 @@ pub async fn get_deps( } #[instrument(level = "trace", skip_all)] -fn install_deps(deps: Vec, verbose: bool) -> Result<()> { +fn install_deps(deps: Vec, verbose: bool, toolchain: &str) -> Result<()> { for dep in deps { match dep { Dependency::Nvm => install_nvm(verbose)?, @@ -472,8 +473,10 @@ fn install_deps(deps: Vec, verbose: bool) -> Result<()> { verbose, )?, Dependency::Rust => install_rust(verbose)?, - Dependency::RustWasm32Wasi => call_rustup("target add wasm32-wasip1", verbose)?, - Dependency::WasmTools => call_cargo("install wasm-tools", verbose)?, + Dependency::RustWasm32Wasi => { + call_rustup("target add wasm32-wasip1", verbose, toolchain)? + } + Dependency::WasmTools => call_cargo("install wasm-tools", verbose, toolchain)?, Dependency::Foundry => install_foundry(verbose)?, Dependency::Docker => {} } @@ -490,6 +493,7 @@ pub async fn execute( javascript_optional: bool, non_interactive: bool, verbose: bool, + toolchain: &str, ) -> Result<()> { info!("Setting up..."); @@ -502,7 +506,7 @@ pub async fn execute( } } - let mut missing_deps = check_rust_deps()?; + let mut missing_deps = check_rust_deps(toolchain)?; let mut js_deps = check_js_deps()?; if !javascript_optional { @@ -527,7 +531,7 @@ pub async fn execute( warn!("Foundry deps are not satisfied: {foundry_deps:?}"); } - get_deps(missing_deps, recv_kill, non_interactive, verbose).await?; + get_deps(missing_deps, recv_kill, non_interactive, verbose, toolchain).await?; info!("Done setting up."); Ok(())