use std::{
borrow::Borrow,
env,
io::{self, BufReader},
process::{Command, ExitStatus, Stdio},
};
use cargo_metadata::{Message, PackageName, camino::Utf8PathBuf};
use miette::Diagnostic;
use snafu::{ResultExt as _, Snafu, ensure};
use tracing::Level;
use crate::{
cargo,
config::Config,
source::{DeserializeAsJsonError, SourceFileLoader, SourceFilePath},
sync::{PackageSyncContext, contents::rustdoc::document::RustdocDocument},
traits::CommandExt as _,
};
#[derive(Debug, Snafu, Diagnostic)]
pub(in crate::sync) enum BuildRustdocError {
#[snafu(display("failed to start rustdoc for package `{package}`: {commandline}"))]
StartRustdocProcess {
package: PackageName,
commandline: String,
#[snafu(source)]
source: io::Error,
},
#[snafu(display("failed to read rustdoc output for package `{package}`: {commandline}"))]
ReadRustdocOutput {
package: PackageName,
commandline: String,
#[snafu(source)]
source: io::Error,
},
#[snafu(display(
"failed to wait for rustdoc completion for package `{package}`: {commandline}"
))]
WaitRustdocProcess {
package: PackageName,
commandline: String,
#[snafu(source)]
source: io::Error,
},
#[snafu(display(
"rustdoc exited with status `{status}` for package `{package}`: {commandline}"
))]
NonZeroExitStatus {
package: PackageName,
commandline: String,
status: ExitStatus,
},
#[snafu(display(
"rustdoc did not produce any JSON output files for package `{package}`: {commandline}"
))]
NoRustdocJsonFiles {
package: PackageName,
commandline: String,
},
#[snafu(display(
"rustdoc produced multiple JSON output files for package `{package}`: {commandline}\nfiles: {files:?}"
))]
MultipleRustdocJsonFiles {
package: PackageName,
commandline: String,
files: Vec<Utf8PathBuf>,
},
#[snafu(display("failed to read rustdoc JSON output file for package `{package}`: {path}", path = json.workspace_path))]
ReadRustdocJson {
package: PackageName,
json: SourceFilePath,
#[snafu(source)]
source: io::Error,
},
#[snafu(display("failed to parse rustdoc JSON output file for package `{package}`: {path}", path = json.workspace_path))]
ParseRustdocJson {
package: PackageName,
json: SourceFilePath,
#[snafu(source)]
source: DeserializeAsJsonError,
},
}
impl Borrow<dyn Diagnostic> for Box<BuildRustdocError> {
fn borrow(&self) -> &(dyn Diagnostic + 'static) {
self.as_ref()
}
}
pub(super) fn build_rustdoc(
cx: &PackageSyncContext<'_>,
) -> Result<RustdocDocument, Box<BuildRustdocError>> {
let json_path = run_rustdoc(cx)?;
let json_file_loader = SourceFileLoader::from_path(cx.workspace, &json_path);
let json_file = json_file_loader
.load()
.with_context(|_source| ReadRustdocJsonSnafu {
package: cx,
json: &json_file_loader,
})?;
let doc = json_file
.deserialize_as_json()
.with_context(|_source| ParseRustdocJsonSnafu {
package: cx,
json: &json_file_loader,
})?;
let doc = RustdocDocument::new(doc);
Ok(doc)
}
fn run_rustdoc(cx: &PackageSyncContext<'_>) -> Result<Utf8PathBuf, Box<BuildRustdocError>> {
let config = &cx.config.rustdoc;
let mut command =
cargo::command_for_build_doc(config.toolchain.as_deref(), cx.install_toolchain);
match cx.verbosity {
Some(Level::TRACE) => _ = command.arg("-vv"),
Some(Level::DEBUG) => _ = command.arg("-v"),
Some(Level::INFO) => {}
_ => _ = command.arg("-q"),
}
command.arg("rustdoc");
command.arg("-Zunstable-options");
command.flag_value("--message-format", "json-render-diagnostics");
command.flag_value("--output-format", "json");
command.arg("-Zrustdoc-map");
command.args(["--package", &cx.package.name]);
if let Some(features) = &config.features {
for feature in features {
command.args(["--features", feature]);
}
}
if let Some(true) = config.all_features {
command.arg("--all-features");
}
if let Some(true) = config.no_default_features {
command.arg("--no-default-features");
}
set_rustdoc_args(&mut command, &cx.config);
set_cargo_args(&mut command, &cx.config);
command.stdout(Stdio::piped());
let commandline = command.commandline();
tracing::debug!("executing rustdoc command: {commandline}");
let mut child = command
.spawn()
.with_context(|_source| StartRustdocProcessSnafu {
package: cx,
commandline: &commandline,
})?;
let stdout = BufReader::new(child.stdout.take().unwrap());
let mut json_filenames = vec![];
for message in Message::parse_stream(stdout) {
let message = message.with_context(|_source| ReadRustdocOutputSnafu {
package: cx,
commandline: &commandline,
})?;
if let Message::CompilerArtifact(artifact) = message
&& artifact.package_id == cx.package.id
{
json_filenames.extend(
artifact
.filenames
.into_iter()
.filter(|f| f.extension().is_some_and(|e| e == "json")),
);
}
}
let status = child
.wait()
.with_context(|_source| WaitRustdocProcessSnafu {
package: cx,
commandline: &commandline,
})?;
ensure!(
status.success(),
NonZeroExitStatusSnafu {
package: cx,
commandline: &commandline,
status,
}
);
ensure!(
json_filenames.len() <= 1,
MultipleRustdocJsonFilesSnafu {
package: cx,
commandline: &commandline,
files: json_filenames.clone(),
}
);
let Some(output_file) = json_filenames.pop() else {
return Err(NoRustdocJsonFilesSnafu {
package: cx,
commandline: &commandline,
}
.build()
.into());
};
Ok(output_file)
}
fn set_rustdoc_args(command: &mut Command, config: &Config) {
const ENCODED_FLAGS_ENV: &str = "CARGO_ENCODED_RUSTDOCFLAGS";
const FLAGS_ENV: &str = "RUSTDOCFLAGS";
let mut flags = vec![];
if let Ok(value) = env::var(ENCODED_FLAGS_ENV) {
if !value.is_empty() {
flags.extend(value.split('\x1f').map(ToOwned::to_owned));
}
} else if let Ok(value) = env::var(FLAGS_ENV) {
flags.extend(
value
.split_whitespace()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(ToOwned::to_owned),
);
}
if let Some(args) = &config.rustdoc.rustdoc_args {
flags.extend_from_slice(args);
}
if !flags.is_empty() {
command.flag_value(
"--config",
format!(
"build.rustdocflags={}",
toml::Value::try_from(&flags).unwrap()
),
);
}
command.env_remove(ENCODED_FLAGS_ENV);
command.env_remove(FLAGS_ENV);
}
fn set_cargo_args(command: &mut Command, config: &Config) {
if let Some(args) = &config.rustdoc.cargo_args {
command.args(args);
}
}