use std::process::Command;
use std::sync::mpsc;
use std::sync::mpsc::TryRecvError;
use std::thread;
use anyhow::{Result, bail};
use esp_generate::template::{GeneratorOption, GeneratorOptionItem};
use esp_metadata::Chip;
use crate::check;
pub struct ToolchainScan {
rx: mpsc::Receiver<Result<Vec<String>>>,
cached: Option<Result<Vec<String>>>,
}
impl ToolchainScan {
pub fn try_get_toolchain_list(&mut self) -> Option<&Result<Vec<String>>> {
if self.cached.is_some() {
return self.cached.as_ref();
}
match self.rx.try_recv() {
Ok(res) => {
self.cached = Some(res);
self.cached.as_ref()
}
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => {
log::warn!(
"Toolchain scan thread failed or channel disconnected; treating as no toolchains"
);
self.cached = Some(Ok(Vec::new()));
self.cached.as_ref()
}
}
}
}
pub fn start_toolchain_scan(
chip: Chip,
cli_toolchain: Option<String>,
msrv: check::Version,
) -> ToolchainScan {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let cli_ref = cli_toolchain.as_deref();
let result = find_toolchains(chip, cli_ref, &msrv);
let _ = tx.send(result);
});
ToolchainScan { rx, cached: None }
}
fn filter_toolchains_for(target: &str, msrv: &check::Version) -> Result<Vec<String>> {
let output = match Command::new("rustup").args(["toolchain", "list"]).output() {
Ok(res) => res,
Err(err) => {
log::warn!("Failed to run `rustup toolchain list`: {err}");
return Ok(Vec::new());
}
};
if !output.status.success() {
log::warn!(
"`rustup toolchain list` exited with status {:?}",
output.status.code()
);
return Ok(Vec::new());
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut available = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let Some(name) = line.split_whitespace().next() else {
continue;
};
if toolchain_matches_target_and_msrv(name, target, msrv) {
available.push(name.to_string());
}
}
Ok(available)
}
fn toolchain_matches_target_and_msrv(name: &str, target: &str, msrv: &check::Version) -> bool {
let output = match Command::new("rustc")
.args([
format!("+{name}"),
"--print".to_string(),
"target-list".to_string(),
])
.output()
{
Ok(res) => res,
Err(err) => {
log::warn!("Failed to run `rustc +{name} --print target-list`: {err}");
return false;
}
};
if !output.status.success() {
log::warn!(
"`rustc +{name} --print target-list` exited with status {:?}",
output.status.code()
);
return false;
}
if !String::from_utf8_lossy(&output.stdout)
.lines()
.any(|l| l.trim() == target)
{
return false;
}
if let Some(ver) = check::get_version("rustc", &[&format!("+{name}")]) {
if !ver.is_at_least(msrv) {
return false;
}
} else {
log::warn!("Failed to detect rustc version for toolchain `{name}`; skipping MSRV check");
}
true
}
fn active_rustup_toolchain() -> Option<String> {
let output = Command::new("rustup")
.args(["show", "active-toolchain"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
String::from_utf8_lossy(&output.stdout)
.lines()
.next()
.and_then(|line| line.split_whitespace().next().map(|name| name.to_string()))
}
pub(crate) fn find_toolchains(
chip: Chip,
cli_toolchain: Option<&str>,
msrv: &check::Version,
) -> Result<Vec<String>> {
let target = chip.target().to_string();
let mut available = filter_toolchains_for(&target, msrv)?;
if chip.is_xtensa() {
available.retain(|name| {
!(name.starts_with("stable") || name.starts_with("beta") || name.starts_with("nightly"))
});
}
if available.is_empty() {
if let Some(cli) = cli_toolchain {
if chip.is_xtensa()
&& (cli.starts_with("stable")
|| cli.starts_with("beta")
|| cli.starts_with("nightly"))
{
bail!(
"Toolchain `{cli}` is not supported for Xtensa targets; \
please use different toolchain (e.g. `esp`, see https://docs.espressif.com/projects/rust/book/getting-started/toolchain.html#xtensa-devices)"
);
}
bail!(
"Toolchain `{cli}` does not have target `{target}` installed (or no toolchain does).\
See https://docs.espressif.com/projects/rust/book/getting-started/toolchain.html"
);
}
log::warn!(
"No rustc toolchains found that have `{target}` installed; toolchain category will stay as placeholder"
);
return Ok(Vec::new());
}
if let Some(cli) = cli_toolchain {
if !available.iter().any(|t| t == cli) {
if chip.is_xtensa()
&& (cli.starts_with("stable")
|| cli.starts_with("beta")
|| cli.starts_with("nightly"))
{
bail!(
"Toolchain `{cli}` is not supported for Xtensa targets; \
please use an ESP toolchain (e.g. `esp`)"
);
}
bail!("Toolchain `{cli}` does not have target `{target}` installed");
}
available.sort();
available.sort_by_key(|t| if t == cli { 0 } else { 1 });
}
Ok(available)
}
pub(crate) fn populate_toolchain_category_from_list(
options: &mut [GeneratorOptionItem],
flat_options: &mut Vec<GeneratorOption>,
available: &[String],
) -> Result<()> {
if available.is_empty() {
return Ok(());
}
let default = active_rustup_toolchain();
for item in options.iter_mut() {
let GeneratorOptionItem::Category(category) = item else {
continue;
};
if category.name != "toolchain" {
continue;
}
let template_opt = match category.options.first() {
Some(GeneratorOptionItem::Option(opt)) => opt.clone(),
_ => {
panic!("toolchain category must contain a placeholder !Option");
}
};
category.options.clear();
for toolchain in available {
let mut opt = template_opt.clone();
let is_default = default.as_deref() == Some(toolchain.as_str());
opt.name = toolchain.clone();
opt.display_name = if is_default {
format!("Use `{toolchain}` toolchain [default]")
} else {
format!("Use `{toolchain}` toolchain")
};
opt.selection_group = "toolchain".to_string();
category
.options
.push(GeneratorOptionItem::Option(opt.clone()));
flat_options.push(opt);
}
break;
}
Ok(())
}