diff --git a/Cargo.lock b/Cargo.lock index c8655fc..c93d873 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2923,6 +2923,7 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "macros" version = "0.2.0" +source = "git+https://github.com/tenxhq/tenx-mcp.git?rev=d95ebe2#d95ebe2218a3822908eb899155591042656fb031" dependencies = [ "heck", "proc-macro2", @@ -4404,6 +4405,7 @@ dependencies = [ [[package]] name = "tenx-mcp" version = "0.0.1" +source = "git+https://github.com/tenxhq/tenx-mcp.git?rev=d95ebe2#d95ebe2218a3822908eb899155591042656fb031" dependencies = [ "async-stream", "async-trait", diff --git a/README.md b/README.md index 68b965f..56175c4 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ The `ruskel_skeleton` tool accepts the following parameters: - `features`: Array of features to enable (default: []) - `quiet`: Enable quiet mode (default: false) - `offline`: Enable offline mode (default: false) +- `target_arch`: Target architecture/platform triple (optional) --- @@ -143,6 +144,18 @@ For standard library support, also install: rustup component add --toolchain nightly rust-docs-json ``` +### Cross-compilation Support + +If you need to generate documentation for a different target architecture, install the nightly toolchain for that target: +```sh +rustup toolchain install nightly --target +``` + +For example, to support ARM64 Linux: +```sh +rustup toolchain install nightly --target aarch64-unknown-linux-gnu +``` + --- ## Installation @@ -197,6 +210,10 @@ ruskel /my/path::foo # A crate from crates.io with a specific version ruskel serde@1.0.0 + +# Generate documentation for a specific target architecture +ruskel --target-arch x86_64-unknown-linux-gnu serde +ruskel --target-arch aarch64-unknown-linux-gnu std::vec::Vec ``` ### Standard Library Support diff --git a/crates/libruskel/src/cargoutils.rs b/crates/libruskel/src/cargoutils.rs index b1d436f..3f5460f 100644 --- a/crates/libruskel/src/cargoutils.rs +++ b/crates/libruskel/src/cargoutils.rs @@ -11,19 +11,25 @@ use std::collections::HashMap; use tempfile::TempDir; use super::target::{Entrypoint, Target}; -use crate::error::{Result, RuskelError, convert_cargo_error}; +use crate::error::{Result, RuskelError, convert_cargo_error, nightly_install_error}; /// Get the sysroot path for the nightly toolchain -fn get_sysroot() -> Result { +fn get_sysroot(target_arch: Option<&str>) -> Result { + let mut args = vec!["+nightly", "--print", "sysroot"]; + if let Some(target) = target_arch { + args.extend(["--target", target]); + } + let output = Command::new("rustc") - .args(["+nightly", "--print", "sysroot"]) + .args(args) .output() .map_err(|e| RuskelError::Generate(format!("Failed to get sysroot: {e}")))?; if !output.status.success() { - return Err(RuskelError::Generate( - "Failed to get nightly sysroot - ensure nightly toolchain is installed".to_string(), - )); + return Err(RuskelError::Generate(nightly_install_error( + "Failed to get nightly sysroot", + target_arch, + ))); } let sysroot = String::from_utf8(output.stdout) @@ -159,8 +165,12 @@ fn resolve_std_reexport(target_str: &str) -> Option { } /// Load pre-built JSON documentation for a standard library crate -fn load_std_library_json(crate_name: &str, display_name: Option<&str>) -> Result { - let sysroot = get_sysroot()?; +fn load_std_library_json( + crate_name: &str, + display_name: Option<&str>, + target_arch: Option<&str>, +) -> Result { + let sysroot = get_sysroot(target_arch)?; let json_path = sysroot .join("share") .join("doc") @@ -219,6 +229,7 @@ impl CargoPath { all_features: bool, features: Vec, silent: bool, + target_arch: Option<&str>, ) -> Result { // Handle standard library crates specially if let CargoPath::StdLibrary(actual_crate, display_crate) = self { @@ -227,7 +238,7 @@ impl CargoPath { } else { None }; - return load_std_library_json(actual_crate, display_name); + return load_std_library_json(actual_crate, display_name, target_arch); } // First check if this crate has a library target by reading Cargo.toml @@ -245,7 +256,7 @@ impl CargoPath { )); } - let json_path = rustdoc_json::Builder::default() + let mut builder = rustdoc_json::Builder::default() .toolchain("nightly") .manifest_path(self.manifest_path()) .document_private_items(true) @@ -253,25 +264,38 @@ impl CargoPath { .all_features(all_features) .features(&features) .quiet(silent) - .silent(silent) - .build() - .map_err(|e| { - let err_msg = e.to_string(); - if err_msg.contains("no library targets found in package") { - // Return just the error without the "Failed to build" wrapper - RuskelError::Generate("error: no library targets found in package".to_string()) - } else if err_msg.contains("toolchain") && err_msg.contains("is not installed") { - // Handle nightly toolchain not installed error - RuskelError::Generate("ruskel requires the nightly toolchain to be installed - run 'rustup toolchain install nightly'".to_string()) - } else if err_msg.contains("Failed to build rustdoc JSON") && err_msg.contains("see stderr") { - // This is the generic error when rustdoc_json fails, likely due to missing nightly - RuskelError::Generate("ruskel requires the nightly toolchain to be installed - run 'rustup toolchain install nightly'".to_string()) - } else if err_msg.contains("Failed to build rustdoc JSON") { - RuskelError::Generate(err_msg) - } else { - RuskelError::Generate(format!("Failed to build rustdoc JSON: {err_msg}")) - } - })?; + .silent(silent); + + // Add target architecture if specified + if let Some(target) = target_arch { + builder = builder.target(target.to_string()); + } + + let json_path = builder.build().map_err(|e| { + let err_msg = e.to_string(); + if err_msg.contains("no library targets found in package") { + // Return just the error without the "Failed to build" wrapper + RuskelError::Generate("error: no library targets found in package".to_string()) + } else if err_msg.contains("toolchain") && err_msg.contains("is not installed") { + // Handle nightly toolchain not installed error + RuskelError::Generate(nightly_install_error( + "Nightly toolchain is not installed", + target_arch, + )) + } else if err_msg.contains("Failed to build rustdoc JSON") + && err_msg.contains("see stderr") + { + // This is the generic error when rustdoc_json fails, likely due to missing nightly + RuskelError::Generate(nightly_install_error( + "Failed to build rustdoc JSON (likely missing nightly toolchain)", + target_arch, + )) + } else if err_msg.contains("Failed to build rustdoc JSON") { + RuskelError::Generate(err_msg) + } else { + RuskelError::Generate(format!("Failed to build rustdoc JSON: {err_msg}")) + } + })?; let json_content = fs::read_to_string(&json_path)?; let crate_data: Crate = serde_json::from_str(&json_content).map_err(|e| { RuskelError::Generate(format!( @@ -506,9 +530,15 @@ impl ResolvedTarget { all_features: bool, features: Vec, silent: bool, + target_arch: Option<&str>, ) -> Result { - self.package_path - .read_crate(no_default_features, all_features, features, silent) + self.package_path.read_crate( + no_default_features, + all_features, + features, + silent, + target_arch, + ) } pub fn from_target(target: Target, offline: bool) -> Result { diff --git a/crates/libruskel/src/error.rs b/crates/libruskel/src/error.rs index 4de88b9..e375804 100644 --- a/crates/libruskel/src/error.rs +++ b/crates/libruskel/src/error.rs @@ -84,3 +84,13 @@ pub fn convert_cargo_error(error: anyhow::Error) -> RuskelError { RuskelError::CargoError(err_msg) } } + +/// Generate a consistent error message for missing nightly toolchain +pub fn nightly_install_error(context: &str, target_arch: Option<&str>) -> String { + let install_cmd = if let Some(target) = target_arch { + format!("rustup toolchain install nightly --target {target}") + } else { + "rustup toolchain install nightly".to_string() + }; + format!("{context} - run '{install_cmd}'") +} diff --git a/crates/libruskel/src/lib.rs b/crates/libruskel/src/lib.rs index acd7250..1adabaf 100644 --- a/crates/libruskel/src/lib.rs +++ b/crates/libruskel/src/lib.rs @@ -20,6 +20,6 @@ mod render; mod ruskel; mod target; -pub use crate::error::{Result, RuskelError}; +pub use crate::error::{Result, RuskelError, nightly_install_error}; pub use crate::render::Renderer; pub use ruskel::Ruskel; diff --git a/crates/libruskel/src/ruskel.rs b/crates/libruskel/src/ruskel.rs index d0362c2..8228751 100644 --- a/crates/libruskel/src/ruskel.rs +++ b/crates/libruskel/src/ruskel.rs @@ -19,6 +19,9 @@ pub struct Ruskel { /// Whether to suppress output during processing. silent: bool, + + /// Target architecture/platform triple for cross-compilation. + target_arch: Option, } impl Ruskel { @@ -56,6 +59,7 @@ impl Ruskel { offline: false, auto_impls: false, silent: false, + target_arch: None, } } @@ -78,6 +82,12 @@ impl Ruskel { self } + /// Sets the target architecture/platform triple for cross-compilation. + pub fn with_target_arch(mut self, target_arch: Option) -> Self { + self.target_arch = target_arch; + self + } + /// Returns the parsed representation of the crate's API. /// /// # Arguments @@ -95,7 +105,13 @@ impl Ruskel { _private_items: bool, ) -> Result { let rt = resolve_target(target, self.offline)?; - rt.read_crate(no_default_features, all_features, features, self.silent) + rt.read_crate( + no_default_features, + all_features, + features, + self.silent, + self.target_arch.as_deref(), + ) } /// Generates a skeletonized version of the crate as a string of Rust code. @@ -115,7 +131,13 @@ impl Ruskel { private_items: bool, ) -> Result { let rt = resolve_target(target, self.offline)?; - let crate_data = rt.read_crate(no_default_features, all_features, features, self.silent)?; + let crate_data = rt.read_crate( + no_default_features, + all_features, + features, + self.silent, + self.target_arch.as_deref(), + )?; let renderer = Renderer::default() .with_filter(&rt.filter) diff --git a/crates/libruskel/tests/targets.rs b/crates/libruskel/tests/targets.rs index 49ef898..5aabc30 100644 --- a/crates/libruskel/tests/targets.rs +++ b/crates/libruskel/tests/targets.rs @@ -32,3 +32,162 @@ fn test_render_specific_struct() -> Result<()> { Ok(()) } + +#[test] +fn test_target_arch_with_riscv() -> Result<()> { + // Test the specific case from the issue: ESP32-C6 with riscv32imac-unknown-none-elf + let ruskel = Ruskel::new() + .with_silent(true) + .with_target_arch(Some("riscv32imac-unknown-none-elf".to_string())); + + let output = ruskel + .render("esp-hal", false, false, vec!["esp32c6".to_string()], false) + .expect("Failed to render esp-hal with esp32c6 target"); + + // Verify the output contains expected content + assert!( + output.contains("esp_hal"), + "Output should contain 'esp_hal'" + ); + assert!(!output.is_empty(), "Output should not be empty"); + assert!( + output.contains("pub"), + "Output should contain at least one 'pub' declaration" + ); + + Ok(()) +} + +#[test] +fn test_target_arch_configuration() -> Result<()> { + let temp_dir = tempdir()?; + let src_dir = temp_dir.path().join("src"); + let lib_path = src_dir.join("lib.rs"); + let cargo_toml_path = temp_dir.path().join("Cargo.toml"); + + std::fs::create_dir_all(&src_dir)?; + std::fs::write(&lib_path, "pub fn test_fn() {}")?; + std::fs::write( + &cargo_toml_path, + r#" + [package] + name = "test_crate" + version = "0.1.0" + edition = "2021" + "#, + )?; + + let ruskel = Ruskel::new() + .with_silent(true) + .with_target_arch(Some("x86_64-unknown-linux-gnu".to_string())); + + let output = ruskel + .render( + temp_dir.path().to_str().unwrap(), + false, + false, + Vec::new(), + false, + ) + .expect("Failed to render test_crate with x86_64-unknown-linux-gnu target"); + + assert!( + output.contains("pub fn test_fn"), + "Output should contain 'pub fn test_fn'" + ); + assert!( + output.contains("test_crate"), + "Output should contain the crate name" + ); + assert!(!output.is_empty(), "Output should not be empty"); + + Ok(()) +} + +#[test] +fn test_target_arch_with_common_targets() -> Result<()> { + // Test common target architectures that should work + let common_targets = [ + "x86_64-unknown-linux-gnu", + "aarch64-apple-darwin", + "wasm32-unknown-unknown", + ]; + + for target in common_targets { + let ruskel = Ruskel::new() + .with_silent(true) + .with_target_arch(Some(target.to_string())); + + let output = ruskel + .render("serde", false, false, Vec::new(), false) + .expect(&format!("Failed to render serde with target {}", target)); + + // Verify the output contains expected content + assert!( + output.contains("serde"), + "Output should contain 'serde' for target {}", + target + ); + assert!( + output.contains("pub"), + "Output should contain at least one 'pub' declaration for target {}", + target + ); + assert!( + !output.is_empty(), + "Output should not be empty for target {}", + target + ); + } + + Ok(()) +} + +#[test] +fn test_target_arch_with_invalid_target() -> Result<()> { + // Test invalid target architectures - these should fail + let invalid_targets = ["invalid-target-triple", "not-a-target"]; + + for target in invalid_targets { + let ruskel = Ruskel::new() + .with_silent(true) + .with_target_arch(Some(target.to_string())); + + let result = ruskel.render("serde", false, false, Vec::new(), false); + + // Invalid targets should fail - we expect an error + assert!( + result.is_err(), + "Expected error for invalid target {}, but got success", + target + ); + } + + Ok(()) +} + +#[test] +fn test_target_arch_with_features() -> Result<()> { + // Test target arch combined with features + let ruskel = Ruskel::new() + .with_silent(true) + .with_target_arch(Some("x86_64-unknown-linux-gnu".to_string())); + + let output = ruskel + .render("serde", false, true, vec!["derive".to_string()], false) + .expect("Failed to render serde with target arch and features"); + + // Verify the output contains expected content + assert!(output.contains("serde"), "Output should contain 'serde'"); + assert!( + output.contains("pub"), + "Output should contain at least one 'pub' declaration" + ); + assert!( + output.contains("derive") || output.contains("Derive"), + "Output should contain derive-related content" + ); + assert!(!output.is_empty(), "Output should not be empty"); + + Ok(()) +} diff --git a/crates/mcp/Cargo.toml b/crates/mcp/Cargo.toml index e6b9dc8..644c6ba 100644 --- a/crates/mcp/Cargo.toml +++ b/crates/mcp/Cargo.toml @@ -10,8 +10,8 @@ readme.workspace = true [dependencies] libruskel = { workspace = true } -#tenx-mcp = { git = "https://github.com/tenxhq/tenx-mcp.git", rev = "1f49b8f"} -tenx-mcp = { path= "../../../tenx-mcp/crates/tenx-mcp" } +tenx-mcp = { git = "https://github.com/tenxhq/tenx-mcp.git", rev = "d95ebe2"} +# tenx-mcp = { path= "../../../tenx-mcp/crates/tenx-mcp" } tokio = { version = "1", features = ["full"] } async-trait = "0.1" serde = { version = "1", features = ["derive"] } diff --git a/crates/ruskel/src/main.rs b/crates/ruskel/src/main.rs index fc1be40..cdd865f 100644 --- a/crates/ruskel/src/main.rs +++ b/crates/ruskel/src/main.rs @@ -1,5 +1,5 @@ use clap::{Parser, ValueEnum}; -use libruskel::{Ruskel, highlight}; +use libruskel::{Ruskel, highlight, nightly_install_error}; use std::io::{self, IsTerminal, Write}; use std::process::{Command, Stdio}; @@ -76,6 +76,10 @@ struct Cli { #[arg(long, default_value_t = false)] verbose: bool, + /// Target architecture/platform triple (e.g., x86_64-unknown-linux-gnu) + #[arg(long)] + target_arch: Option, + /// Run as an MCP server on stdout #[arg(long, default_value_t = false)] mcp: bool, @@ -89,7 +93,7 @@ struct Cli { log: Option, } -fn check_nightly_toolchain() -> Result<(), String> { +fn check_nightly_toolchain(target_arch: Option<&str>) -> Result<(), String> { // Check if nightly toolchain is installed let output = Command::new("rustup") .args(["run", "nightly", "rustc", "--version"]) @@ -98,7 +102,10 @@ fn check_nightly_toolchain() -> Result<(), String> { .map_err(|e| format!("Failed to run rustup: {e}"))?; if !output.status.success() { - return Err("ruskel requires the nightly toolchain to be installed.\nRun: rustup toolchain install nightly".to_string()); + return Err(nightly_install_error( + "ruskel requires the nightly toolchain to be installed", + target_arch, + )); } // Check if rust-docs-json component is available (for std library support) @@ -136,7 +143,7 @@ fn run_mcp(cli: &Cli) -> Result<(), Box> { || cli.no_page { return Err( - "--mcp can only be used with --auto-impls, --private, --offline, and --verbose".into(), + "--mcp can only be used with --auto-impls, --private, --offline, --verbose, and --target-arch".into(), ); } @@ -144,7 +151,8 @@ fn run_mcp(cli: &Cli) -> Result<(), Box> { let ruskel = Ruskel::new() .with_offline(cli.offline) .with_auto_impls(cli.auto_impls) - .with_silent(!cli.verbose); + .with_silent(!cli.verbose) + .with_target_arch(cli.target_arch.clone()); // Run the MCP server let runtime = tokio::runtime::Runtime::new()?; @@ -176,7 +184,8 @@ fn run_cmdline(cli: &Cli) -> Result<(), Box> { let rs = Ruskel::new() .with_offline(cli.offline) .with_auto_impls(cli.auto_impls) - .with_silent(!cli.verbose); + .with_silent(!cli.verbose) + .with_target_arch(cli.target_arch.clone()); let mut output = if cli.raw { rs.raw_json( @@ -228,7 +237,7 @@ fn main() { let result = if cli.mcp { run_mcp(&cli) } else { - if let Err(e) = check_nightly_toolchain() { + if let Err(e) = check_nightly_toolchain(cli.target_arch.as_deref()) { eprintln!("{e}"); std::process::exit(1); }