Skip to content
Open
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
2 changes: 2 additions & 0 deletions Cargo.lock

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

17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down Expand Up @@ -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 <target-triple>
```

For example, to support ARM64 Linux:
```sh
rustup toolchain install nightly --target aarch64-unknown-linux-gnu
```

---

## Installation
Expand Down Expand Up @@ -197,6 +210,10 @@ ruskel /my/path::foo

# A crate from crates.io with a specific version
ruskel [email protected]

# 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
Expand Down
92 changes: 61 additions & 31 deletions crates/libruskel/src/cargoutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
fn get_sysroot(target_arch: Option<&str>) -> Result<PathBuf> {
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)
Expand Down Expand Up @@ -159,8 +165,12 @@ fn resolve_std_reexport(target_str: &str) -> Option<String> {
}

/// Load pre-built JSON documentation for a standard library crate
fn load_std_library_json(crate_name: &str, display_name: Option<&str>) -> Result<Crate> {
let sysroot = get_sysroot()?;
fn load_std_library_json(
crate_name: &str,
display_name: Option<&str>,
target_arch: Option<&str>,
) -> Result<Crate> {
let sysroot = get_sysroot(target_arch)?;
let json_path = sysroot
.join("share")
.join("doc")
Expand Down Expand Up @@ -219,6 +229,7 @@ impl CargoPath {
all_features: bool,
features: Vec<String>,
silent: bool,
target_arch: Option<&str>,
) -> Result<Crate> {
// Handle standard library crates specially
if let CargoPath::StdLibrary(actual_crate, display_crate) = self {
Expand All @@ -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
Expand All @@ -245,33 +256,46 @@ 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)
.no_default_features(no_default_features)
.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!(
Expand Down Expand Up @@ -506,9 +530,15 @@ impl ResolvedTarget {
all_features: bool,
features: Vec<String>,
silent: bool,
target_arch: Option<&str>,
) -> Result<Crate> {
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<Self> {
Expand Down
10 changes: 10 additions & 0 deletions crates/libruskel/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}'")
}
2 changes: 1 addition & 1 deletion crates/libruskel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
26 changes: 24 additions & 2 deletions crates/libruskel/src/ruskel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}

impl Ruskel {
Expand Down Expand Up @@ -56,6 +59,7 @@ impl Ruskel {
offline: false,
auto_impls: false,
silent: false,
target_arch: None,
}
}

Expand All @@ -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<String>) -> Self {
self.target_arch = target_arch;
self
}

/// Returns the parsed representation of the crate's API.
///
/// # Arguments
Expand All @@ -95,7 +105,13 @@ impl Ruskel {
_private_items: bool,
) -> Result<Crate> {
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.
Expand All @@ -115,7 +131,13 @@ impl Ruskel {
private_items: bool,
) -> Result<String> {
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)
Expand Down
Loading