use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use std::{env, fs};
use anyhow::{anyhow, Context, Error, Result};
use serde::{Deserialize, Serialize};
use crate::python::PYTHON;
use crate::{cmd, git, path_buf, python};
use self::tools_schema::{
PlatformDownloadInfo, PlatformOverrideInfoPlatformsItem, ToolInfo, VersionInfo,
};
#[cfg(feature = "elf")]
pub mod ulp_fsm;
mod tools_schema;
pub const DEFAULT_ESP_IDF_REPOSITORY: &str = "https://github.com/espressif/esp-idf.git";
pub const MANAGED_ESP_IDF_REPOS_DIR_BASE: &str = "esp-idf";
pub const IDF_PATH_VAR: &str = "IDF_PATH";
pub const IDF_TOOLS_PATH_VAR: &str = "IDF_TOOLS_PATH";
const IDF_PYTHON_ENV_PATH_VAR: &str = "IDF_PYTHON_ENV_PATH";
pub const GLOBAL_INSTALL_DIR: &str = ".espressif";
pub const BUILD_INFO_FILENAME: &str = "esp-idf-build.json";
#[derive(Debug, Clone)]
pub struct Tools {
pub index: Option<PathBuf>,
pub tools: Vec<String>,
_tempfile: Option<Arc<tempfile::TempPath>>,
}
impl Tools {
pub fn new(tools: impl IntoIterator<Item = impl AsRef<str>>) -> Tools {
Tools {
index: None,
tools: tools.into_iter().map(|s| s.as_ref().to_owned()).collect(),
_tempfile: None,
}
}
pub fn new_with_index(
iter: impl IntoIterator<Item = impl AsRef<str>>,
tools_json: impl AsRef<Path>,
) -> Tools {
Tools {
index: Some(tools_json.as_ref().into()),
tools: iter.into_iter().map(|s| s.as_ref().to_owned()).collect(),
_tempfile: None,
}
}
pub fn new_with_index_str(
tools: Vec<String>,
tools_json_content: impl AsRef<str>,
) -> Result<Tools> {
let mut temp = tempfile::NamedTempFile::new()?;
temp.as_file_mut()
.write_all(tools_json_content.as_ref().as_bytes())?;
let temp = temp.into_temp_path();
Ok(Tools {
index: Some(temp.to_path_buf()),
tools,
_tempfile: Some(Arc::new(temp)),
})
}
pub fn cmake() -> Result<Tools> {
Self::new_with_index_str(
vec!["cmake".into()],
include_str!("espidf/resources/cmake.json"),
)
}
}
#[derive(Debug, Default)]
struct Tool {
name: String,
url: String,
version: String,
sha256: String,
size: i64,
install_dir: PathBuf,
export_path: PathBuf,
export_vars: HashMap<String, String>,
version_cmd_args: Vec<String>,
version_regex: String,
}
impl Tool {
fn test(&self) -> bool {
let tool_path = self.abs_export_path();
if !tool_path.exists() {
return false;
}
if let Some(mut test_command) = self.test_command() {
log::debug!("Run cmd: {test_command:?} to get current tool version");
let output = test_command.output().unwrap_or_else(|e| {
panic!("Failed to run command: {test_command:?}; error: {e:?}")
});
if !self.version_regex.is_empty() {
let regex =
regex::Regex::new(&self.version_regex).expect("Invalid regex pattern provided");
if let Some(capture) = regex.captures(&String::from_utf8_lossy(&output.stdout)) {
if let Some(var) = capture.get(0) {
log::debug!("Match: {:?}, Version: {:?}", &var.as_str(), &self.version);
return true;
}
}
false
} else {
true
}
} else {
true
}
}
fn abs_export_path(&self) -> PathBuf {
self.install_dir.join(self.export_path.as_path())
}
fn abs_export_env_vars(&self) -> impl Iterator<Item = (String, String)> + '_ {
self.export_vars.iter().map(|(var, value)| {
let value = value.replace("${TOOL_PATH}", self.abs_export_path().to_str().unwrap());
(var.clone(), value)
})
}
fn test_command(&self) -> Option<Command> {
(!self.version_cmd_args.is_empty() && !self.version_cmd_args[0].is_empty()).then(|| {
let cmd_abs_path = self
.abs_export_path()
.join(self.version_cmd_args[0].clone());
let mut version_cmd = std::process::Command::new(cmd_abs_path);
version_cmd.args(self.version_cmd_args[1..].iter().cloned());
version_cmd
})
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct ToolsInfo {
tools: Vec<ToolInfo>,
version: u32,
}
fn parse_tools(
tools_wanted: Vec<&str>,
tools_json_file: PathBuf,
install_dir: PathBuf,
) -> anyhow::Result<Vec<Tool>> {
let mut tools_string = String::new();
let mut tools_file = std::fs::File::open(tools_json_file)?;
tools_file.read_to_string(&mut tools_string)?;
let tools_info = serde_json::from_str::<ToolsInfo>(&tools_string)?;
let tools = tools_info.tools;
let tools = tools.iter().filter(|tool_info|{
tools_wanted.contains(&tool_info.name.as_ref().unwrap().as_str())
}).map(|tool_info| {
let mut tool = Tool {
name: tool_info.name.as_ref().unwrap().clone(),
install_dir: install_dir.clone(),
version_cmd_args: tool_info.version_cmd.to_vec(),
version_regex: tool_info.version_regex.to_string(),
export_vars: tool_info.export_vars.as_ref().map(|v| v.0.clone()).unwrap_or_default(),
..Default::default()
};
tool_info.versions.iter().filter(|version| {
version.status == Some(tools_schema::VersionInfoStatus::Recommended)
}).for_each(|version| {
let os_matcher = |info: &VersionInfo| -> Option<PlatformDownloadInfo> {
let os = std::env::consts::OS;
let arch = std::env::consts::ARCH;
match (os, arch) {
("linux", "x86") => info.linux_i686.clone(),
("linux", "x86_64") => info.linux_amd64.clone(),
("linux", "arm") => info.linux_armel.clone(),
("linux", "aarch64") => info.linux_arm64.clone(),
("macos", "x86_64") => info.macos.clone(),
("macos", "aarch64") => info.macos_arm64.clone(),
("windows", "x86") => info.win32.clone(),
("windows", "x86_64") => info.win64.clone(),
_ => None,
}
};
let info = if let Some(plaform_dll_info) = version.any.clone() {
plaform_dll_info
} else if let Some(plaform_dll_info) = os_matcher(version) {
plaform_dll_info
} else {
panic!("Neither any or platform specifc match found. Please create an issue on https://github.com/esp-rs/embuild and report your operating system");
};
tool.url = info.url;
tool.sha256 = info.sha256;
tool.size = info.size;
tool.version.clone_from(version.name.as_ref().unwrap());
tool.export_path = PathBuf::new().join("tools").join(&tool.name).join(&tool.version);
let first_path = tool_info.export_paths.first();
if let Some(path) = first_path {
for element in path.iter() {
if !element.is_empty() {
tool.export_path = tool.export_path.join(element);
}
}
}
});
let platform = match (std::env::consts::OS, std::env::consts::ARCH) {
("linux", "x86") => Some(PlatformOverrideInfoPlatformsItem::LinuxI686),
("linux", "x86_64") => Some(PlatformOverrideInfoPlatformsItem::LinuxAmd64),
("linux", "arm") => Some(PlatformOverrideInfoPlatformsItem::LinuxArmel),
("linux", "aarch64") => Some(PlatformOverrideInfoPlatformsItem::LinuxArm64),
("macos", "x86_64") => Some(PlatformOverrideInfoPlatformsItem::Macos),
("macos", "aarch64") => Some(PlatformOverrideInfoPlatformsItem::MacosArm64),
("windows", "x86") => Some(PlatformOverrideInfoPlatformsItem::Win32),
("windows", "x86_64") => Some(PlatformOverrideInfoPlatformsItem::Win64),
_ => None,
};
if let Some(p) = platform {
tool_info.platform_overrides
.iter()
.filter(|info| info.platforms.contains(&p))
.for_each(|info| {
if let Some(export_path) = &info.export_paths {
if let Some(first_path) = export_path.first() {
tool.export_path = PathBuf::from_iter(
["tools", &tool.name, &tool.version].into_iter()
.chain(first_path.iter().map(String::as_str))
);
}
}
if let Some(version_cmd) = &info.version_cmd {
tool.version_cmd_args = version_cmd.to_vec();
}
if let Some(version_regex) = &info.version_regex {
tool.version_regex = version_regex.to_string();
}
});
}
log::debug!("{tool:?}");
tool
}
).collect();
Ok(tools)
}
#[derive(Debug, thiserror::Error)]
pub enum FromEnvError {
#[error("could not detect `esp-idf` repository in the environment")]
NoRepo(#[source] anyhow::Error),
#[error("`esp-idf` repository exists but required tools not in environment")]
NotActivated(
#[from]
#[source]
NotActivatedError,
),
}
#[derive(Debug, thiserror::Error)]
#[error("Error activating `esp-idf` tools")]
pub struct NotActivatedError {
pub esp_idf_dir: SourceTree,
#[source]
pub source: anyhow::Error,
}
#[derive(Debug)]
pub struct EspIdf {
pub esp_idf_dir: SourceTree,
pub exported_path: OsString,
pub exported_env_vars: HashMap<String, String>,
pub venv_python: PathBuf,
pub version: Result<EspIdfVersion>,
pub is_managed_espidf: bool,
}
#[derive(Debug, Clone)]
pub enum SourceTree {
Git(git::Repository),
Plain(PathBuf),
}
impl SourceTree {
pub fn open(path: &Path) -> Self {
git::Repository::open(path)
.map(SourceTree::Git)
.unwrap_or_else(|_| SourceTree::Plain(path.to_owned()))
}
pub fn path(&self) -> &Path {
match self {
SourceTree::Git(repo) => repo.worktree(),
SourceTree::Plain(path) => path,
}
}
}
impl EspIdf {
pub fn try_from(idf_path: &Path) -> Result<EspIdf, NotActivatedError> {
let esp_idf_dir = SourceTree::open(idf_path);
let path_var = env::var_os("PATH").unwrap_or_default();
let not_activated = |source: Error| -> NotActivatedError {
NotActivatedError {
esp_idf_dir: esp_idf_dir.clone(),
source,
}
};
let idf_py = if cfg!(windows) {
env::split_paths(&path_var)
.find_map(|p| {
let file_path = Path::new(&p).join("idf.py");
if file_path.is_file() {
Some(file_path)
} else {
None
}
})
.ok_or(which::Error::CannotFindBinaryPath)
} else {
which::which_in("idf.py", Some(&path_var), "")
}
.with_context(|| anyhow!("could not find `idf.py` in $PATH"))
.map_err(not_activated)?;
let idf_py_repo = path_buf![esp_idf_dir.path(), "tools", "idf.py"];
match (idf_py.canonicalize(), idf_py_repo.canonicalize()) {
(Ok(a), Ok(b)) if a != b => {
return Err(not_activated(anyhow!(
"missmatch between tools in $PATH ('{}') and esp-idf repository \
given by ${IDF_PATH_VAR} ('{}')",
a.display(),
b.display()
)))
}
_ => (),
};
let python = which::which_in("python", Some(&path_var), "")
.with_context(|| anyhow!("python not found in $PATH"))
.map_err(not_activated)?;
let check_python_deps_py =
path_buf![esp_idf_dir.path(), "tools", "check_python_dependencies.py"];
cmd!(&python, &check_python_deps_py)
.stdout()
.with_context(|| anyhow!("failed to check python dependencies"))
.map_err(not_activated)?;
Ok(EspIdf {
version: EspIdfVersion::try_from(esp_idf_dir.path()),
esp_idf_dir,
exported_path: path_var,
exported_env_vars: HashMap::new(),
venv_python: python,
is_managed_espidf: true,
})
}
pub fn try_from_env() -> Result<EspIdf, FromEnvError> {
let idf_path = env::var_os(IDF_PATH_VAR)
.map(PathBuf::from)
.ok_or_else(|| {
FromEnvError::NoRepo(anyhow!("environment variable `{IDF_PATH_VAR}` not found"))
})?;
let idf = Self::try_from(&idf_path)?;
Ok(idf)
}
}
#[derive(Clone, Debug)]
pub struct EspIdfVersion {
pub major: u64,
pub minor: u64,
pub patch: u64,
}
impl EspIdfVersion {
pub fn try_from(esp_idf_dir: &Path) -> Result<Self> {
let version_cmake = path_buf![esp_idf_dir, "tools", "cmake", "version.cmake"];
let base_err = || {
anyhow!(
"could not determine esp-idf version from '{}'",
version_cmake.display()
)
};
let s = fs::read_to_string(&version_cmake).with_context(base_err)?;
let mut ver = [None; 3];
s.lines()
.filter_map(|l| {
l.trim()
.strip_prefix("set")?
.trim_start()
.strip_prefix('(')?
.strip_suffix(')')?
.split_once(' ')
})
.fold((), |_, (key, value)| {
let index = match key.trim() {
"IDF_VERSION_MAJOR" => 0,
"IDF_VERSION_MINOR" => 1,
"IDF_VERSION_PATCH" => 2,
_ => return,
};
if let Ok(val) = value.trim().parse::<u64>() {
ver[index] = Some(val);
}
});
if let [Some(major), Some(minor), Some(patch)] = ver {
Ok(Self {
major,
minor,
patch,
})
} else {
Err(anyhow!("parsing failed").context(base_err()))
}
}
pub fn format(ver: &Result<EspIdfVersion>) -> String {
match ver {
Ok(v) => format!("v{v}"),
Err(_) => "(unknown version)".to_string(),
}
}
}
impl std::fmt::Display for EspIdfVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
pub enum EspIdfOrigin {
Managed(git::sdk::RemoteSdk),
Custom(SourceTree),
}
pub type EspIdfRemote = git::sdk::RemoteSdk;
pub struct Installer {
esp_idf_origin: EspIdfOrigin,
custom_install_dir: Option<PathBuf>,
#[allow(clippy::type_complexity)]
tools_provider:
Option<Box<dyn FnOnce(&SourceTree, &Result<EspIdfVersion>) -> Result<Vec<Tools>>>>,
}
impl Installer {
pub fn new(esp_idf_origin: EspIdfOrigin) -> Installer {
Self {
esp_idf_origin,
tools_provider: None,
custom_install_dir: None,
}
}
#[must_use]
pub fn with_tools<F>(mut self, provider: F) -> Self
where
F: 'static + FnOnce(&SourceTree, &Result<EspIdfVersion>) -> Result<Vec<Tools>>,
{
self.tools_provider = Some(Box::new(provider));
self
}
#[must_use]
pub fn install_dir(mut self, install_dir: Option<PathBuf>) -> Self {
self.custom_install_dir = install_dir;
self
}
pub fn install(self) -> Result<EspIdf> {
let install_dir = self
.custom_install_dir
.unwrap_or_else(Self::global_install_dir);
std::fs::create_dir_all(&install_dir).with_context(|| {
format!(
"could not create esp-idf install dir '{}'",
install_dir.display()
)
})?;
let (esp_idf_dir, managed_repo) = match self.esp_idf_origin {
EspIdfOrigin::Managed(managed) => (
SourceTree::Git(managed.open_or_clone(
&install_dir,
git::CloneOptions::new().depth(1),
DEFAULT_ESP_IDF_REPOSITORY,
MANAGED_ESP_IDF_REPOS_DIR_BASE,
)?),
true,
),
EspIdfOrigin::Custom(tree) => (tree, false),
};
let esp_version = EspIdfVersion::try_from(esp_idf_dir.path())?;
let python_version = python::check_python_at_least(3, 6)?;
let idf_tools_py = path_buf![esp_idf_dir.path(), "tools", "idf_tools.py"];
cmd!(PYTHON, &idf_tools_py, "--idf-path", esp_idf_dir.path(), "--non-interactive", "install-python-env";
env=(IDF_TOOLS_PATH_VAR, &install_dir), env_remove=("MSYSTEM"), env_remove=(IDF_PYTHON_ENV_PATH_VAR)).run()?;
let idf_major_minor = format!("{}.{}", esp_version.major, esp_version.minor);
let python_major_minor = format!("{}.{}", python_version.major, python_version.minor);
let python_env_dir_template = format!("idf{idf_major_minor}_py{python_major_minor}_env");
let python_env_dir = path_buf![&install_dir, "python_env", python_env_dir_template];
let esp_version = Ok(esp_version);
#[cfg(windows)]
let venv_python = PathBuf::from(python_env_dir).join("Scripts/python");
#[cfg(not(windows))]
let venv_python = python_env_dir.join("bin/python");
log::debug!("Start installing tools");
let tools = self
.tools_provider
.map(|p| p(&esp_idf_dir, &esp_version))
.unwrap_or(Ok(Vec::new()))?;
let tools_wanted = tools.clone();
let tools_wanted: Vec<&str> = tools_wanted
.iter()
.flat_map(|tool| tool.tools.iter().map(|s| s.as_str()))
.collect();
let tools_json = esp_idf_dir.path().join("tools/tools.json");
let tools_vec = parse_tools(
tools_wanted.clone(),
tools_json.clone(),
install_dir.clone(),
)
.unwrap();
let all_tools_installed = tools_vec.iter().all(|tool| tool.test());
if !all_tools_installed {
for tool_set in tools {
let tools_json = tool_set
.index
.as_ref()
.map(|tools_json| {
[OsStr::new("--tools-json"), tools_json.as_os_str()].into_iter()
})
.into_iter()
.flatten();
cmd!(&venv_python, &idf_tools_py, "--idf-path", esp_idf_dir.path(), @tools_json.clone(), "install";
env=(IDF_TOOLS_PATH_VAR, &install_dir), args=(tool_set.tools)).run()?;
}
let all_tools_installed = tools_vec.iter().all(|tool| tool.test());
if !all_tools_installed {
return Err(anyhow::Error::msg("Could not install all requested Tools"));
}
}
let mut tools_path: Vec<PathBuf> = tools_vec
.iter()
.map(|tool| tool.abs_export_path())
.collect();
let mut python_path = venv_python.clone();
python_path.pop();
tools_path.push(python_path);
let paths = env::join_paths(
tools_path
.into_iter()
.chain(env::split_paths(&env::var_os("PATH").unwrap_or_default())),
)?;
let env_vars = tools_vec
.iter()
.flat_map(|tool| tool.abs_export_env_vars())
.collect::<HashMap<_, _>>();
log::debug!("Using PATH='{}'", &paths.to_string_lossy());
Ok(EspIdf {
esp_idf_dir,
exported_path: paths,
exported_env_vars: env_vars,
venv_python,
version: esp_version,
is_managed_espidf: managed_repo,
})
}
pub fn global_install_dir() -> PathBuf {
home::home_dir()
.expect("No home directory available for this operating system")
.join(GLOBAL_INSTALL_DIR)
}
}
pub fn parse_esp_idf_git_ref(version: &str) -> git::Ref {
git::Ref::parse(version)
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct EspIdfBuildInfo {
pub esp_idf_dir: PathBuf,
pub exported_path_var: String,
pub venv_python: PathBuf,
pub build_dir: PathBuf,
pub project_dir: PathBuf,
pub compiler: PathBuf,
pub mcu: String,
pub sdkconfig: Option<PathBuf>,
pub sdkconfig_defaults: Option<Vec<PathBuf>>,
}
impl EspIdfBuildInfo {
pub fn from_json(path: impl AsRef<Path>) -> Result<EspIdfBuildInfo> {
let file = std::fs::File::open(&path)
.with_context(|| anyhow!("Could not read {}", path.as_ref().display()))?;
let result: EspIdfBuildInfo = serde_json::from_reader(file)?;
Ok(result)
}
pub fn save_json(&self, path: impl AsRef<Path>) -> Result<()> {
let file = std::fs::File::create(&path)
.with_context(|| anyhow!("Could not write {}", path.as_ref().display()))?;
serde_json::to_writer_pretty(file, self)?;
Ok(())
}
}
pub mod sysenv {
use std::env;
use crate::{
build::{CInclArgs, CfgArgs, LinkArgs},
cargo,
};
const CRATES_LINKS_LIBS: [&str; 3] = ["ESP_IDF_SVC", "ESP_IDF_HAL", "ESP_IDF"];
pub fn cfg_args() -> Option<CfgArgs> {
CRATES_LINKS_LIBS
.iter()
.filter_map(|lib| CfgArgs::try_from_env(lib).ok())
.next()
}
pub fn cincl_args() -> Option<CInclArgs> {
CRATES_LINKS_LIBS
.iter()
.filter_map(|lib| CInclArgs::try_from_env(lib).ok())
.next()
}
pub fn link_args() -> Option<LinkArgs> {
CRATES_LINKS_LIBS
.iter()
.filter_map(|lib| LinkArgs::try_from_env(lib).ok())
.next()
}
pub fn env_path() -> Option<String> {
CRATES_LINKS_LIBS
.iter()
.filter_map(|lib| env::var(format!("DEP_{lib}_{}", crate::build::ENV_PATH_VAR)).ok())
.next()
}
pub fn idf_path() -> Option<String> {
CRATES_LINKS_LIBS
.iter()
.filter_map(|lib| {
env::var(format!("DEP_{lib}_{}", crate::build::ESP_IDF_PATH_VAR)).ok()
})
.next()
}
pub fn relay() {
if let Some(args) = cfg_args() {
args.propagate()
}
if let Some(args) = cincl_args() {
args.propagate()
}
if let Some(args) = link_args() {
args.propagate()
}
if let Some(path) = env_path() {
cargo::set_metadata(crate::build::ENV_PATH_VAR, path)
}
if let Some(path) = idf_path() {
cargo::set_metadata(crate::build::ESP_IDF_PATH_VAR, path)
}
}
pub fn output() {
if let Some(args) = cfg_args() {
args.output()
}
if let Some(args) = link_args() {
args.output()
}
}
}