use std::collections::HashMap;
use std::env;
use std::path::Path;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::fs;
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::io::{self, BufRead, BufReader};
use std::sync::{Arc, Mutex};
use remove_dir_all::remove_dir_contents;
use crossbeam::thread;
pub fn default_esp_idf_version() -> String {
"5.5.2".to_string()
}
#[derive(Debug, Clone)]
pub struct BuildInfo {
pub last_built_systype: Option<String>,
pub last_build_method: Option<String>, pub last_idf_path_explicit: bool,
pub last_idf_path: Option<String>,
pub last_port: Option<String>,
pub last_monitor_baud: Option<u32>,
pub last_flash_baud: Option<u32>,
pub last_vid: Option<String>,
pub last_no_fs: Option<bool>,
}
impl Default for BuildInfo {
fn default() -> Self {
BuildInfo {
last_built_systype: None,
last_build_method: None,
last_idf_path_explicit: false,
last_idf_path: None,
last_port: None,
last_monitor_baud: None,
last_flash_baud: None,
last_vid: None,
last_no_fs: None,
}
}
}
pub fn read_build_info(app_folder: &str) -> BuildInfo {
let raft_info_path = format!("{}/build/raft.info", app_folder);
if let Ok(contents) = fs::read_to_string(&raft_info_path) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&contents) {
return BuildInfo {
last_built_systype: json["last_built_systype"].as_str().map(|s| s.to_string()),
last_build_method: json["last_build_method"].as_str().map(|s| s.to_string()),
last_idf_path_explicit: json["last_idf_path_explicit"].as_bool().unwrap_or(false),
last_idf_path: json["last_idf_path"].as_str().map(|s| s.to_string()),
last_port: json["last_port"].as_str().map(|s| s.to_string()),
last_monitor_baud: json["last_monitor_baud"].as_u64().map(|v| v as u32),
last_flash_baud: json["last_flash_baud"].as_u64().map(|v| v as u32),
last_vid: json["last_vid"].as_str().map(|s| s.to_string()),
last_no_fs: json["last_no_fs"].as_bool(),
};
}
}
BuildInfo::default()
}
pub fn write_build_info(
app_folder: &str,
updates: &BuildInfo,
) -> Result<(), Box<dyn std::error::Error>> {
let build_folder = format!("{}/build", app_folder);
if !Path::new(&build_folder).exists() {
fs::create_dir_all(&build_folder)?;
}
let raft_info_path = format!("{}/raft.info", build_folder);
let existing = read_build_info(app_folder);
let merged = BuildInfo {
last_built_systype: updates.last_built_systype.clone().or(existing.last_built_systype),
last_build_method: updates.last_build_method.clone().or(existing.last_build_method),
last_idf_path_explicit: if updates.last_idf_path.is_some() { updates.last_idf_path_explicit } else { existing.last_idf_path_explicit },
last_idf_path: updates.last_idf_path.clone().or(existing.last_idf_path),
last_port: updates.last_port.clone().or(existing.last_port),
last_monitor_baud: updates.last_monitor_baud.or(existing.last_monitor_baud),
last_flash_baud: updates.last_flash_baud.or(existing.last_flash_baud),
last_vid: updates.last_vid.clone().or(existing.last_vid),
last_no_fs: updates.last_no_fs.or(existing.last_no_fs),
};
let mut raft_info = serde_json::json!({
"last_idf_path_explicit": merged.last_idf_path_explicit
});
if let Some(ref v) = merged.last_built_systype { raft_info["last_built_systype"] = serde_json::json!(v); }
if let Some(ref v) = merged.last_build_method { raft_info["last_build_method"] = serde_json::json!(v); }
if let Some(ref v) = merged.last_idf_path { raft_info["last_idf_path"] = serde_json::json!(v); }
if let Some(ref v) = merged.last_port { raft_info["last_port"] = serde_json::json!(v); }
if let Some(v) = merged.last_monitor_baud { raft_info["last_monitor_baud"] = serde_json::json!(v); }
if let Some(v) = merged.last_flash_baud { raft_info["last_flash_baud"] = serde_json::json!(v); }
if let Some(ref v) = merged.last_vid { raft_info["last_vid"] = serde_json::json!(v); }
if let Some(v) = merged.last_no_fs { raft_info["last_no_fs"] = serde_json::json!(v); }
fs::write(&raft_info_path, serde_json::to_string_pretty(&raft_info)?)?;
Ok(())
}
pub fn utils_get_sys_type(
build_sys_type: &Option<String>,
app_folder: String
) -> Result<String, Box<dyn std::error::Error>> {
let mut sys_type: String = String::new();
if let Some(build_sys_type) = build_sys_type {
sys_type = build_sys_type.to_string();
} else {
let build_info = read_build_info(&app_folder);
if let Some(last_systype) = build_info.last_built_systype {
let systype_path = format!("{}/{}/{}", app_folder, get_systypes_folder_name(), last_systype);
if Path::new(&systype_path).exists() {
sys_type = last_systype;
}
}
if sys_type.is_empty() {
let sys_types = fs::read_dir(
format!("{}/{}", app_folder, get_systypes_folder_name())
);
if sys_types.is_err() {
println!("Error reading the systypes folder: {}", sys_types.err().unwrap());
return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "Error reading the systypes folder")));
}
for sys_type_dir_entry in sys_types.unwrap() {
let sys_type_dir = sys_type_dir_entry;
if sys_type_dir.is_err() {
println!("Error reading the systypes folder: {}", sys_type_dir.err().unwrap());
return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "Error reading the systypes folder")));
}
let sys_type_name = sys_type_dir.unwrap().file_name().into_string().unwrap();
if sys_type_name != "Common" {
sys_type = sys_type_name;
break;
}
}
}
}
Ok(sys_type)
}
pub fn check_app_folder_valid(app_folder: String) -> bool {
let cmake_file = format!("{}/CMakeLists.txt", app_folder);
if !Path::new(&app_folder).exists() {
println!("Error: app folder does not exist: {}", app_folder);
false
} else if !Path::new(&cmake_file).exists() {
println!("Error: app folder does not contain a CMakeLists.txt file: {}", app_folder);
false
} else if !Path::new(&format!("{}/{}", app_folder, get_systypes_folder_name())).exists() {
println!("Error: app folder does not contain a systypes folder: {}", app_folder);
false
} else {
true
}
}
pub fn convert_path_for_docker(path: PathBuf) -> Result<String, std::io::Error> {
let path_str = path.into_os_string().into_string().unwrap();
let trimmed_path = if path_str.starts_with("\\\\?\\") {
&path_str[4..]
} else {
&path_str
};
let docker_path = trimmed_path.replace("\\", "/");
println!("Converted path: {} to: {}", path_str, docker_path);
Ok(docker_path)
}
#[derive(Debug)]
pub enum CommandError {
CommandNotFound(String),
ExecutionFailed(String),
Other(io::Error),
}
impl Display for CommandError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self) }
}
impl Error for CommandError {}
pub fn execute_and_capture_output(command: String, args: &Vec<String>, cur_dir: String, env_vars_to_add: HashMap<String, String>) -> Result<(String, bool), CommandError> {
let process = Command::new(command.clone())
.current_dir(cur_dir)
.args(args)
.envs(env_vars_to_add.iter())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn();
let mut process = match process {
Ok(process) => process,
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
return Err(CommandError::CommandNotFound(format!("{}: No such file or directory", command.clone())));
} else {
return Err(CommandError::Other(e));
}
}
};
let stdout = process.stdout.take().unwrap();
let stderr = process.stderr.take().unwrap();
let stdout_reader = BufReader::new(stdout);
let stderr_reader = BufReader::new(stderr);
let captured_output = Arc::new(Mutex::new(String::new()));
let thread_result = thread::scope(|s| {
let captured = Arc::clone(&captured_output);
s.spawn(move |_| {
for line in stdout_reader.lines() {
match line {
Ok(line) => {
println!("{}", line); let mut captured = captured.lock().unwrap();
captured.push_str(&line);
captured.push('\n');
}
Err(_) => break,
}
}
});
let captured = Arc::clone(&captured_output);
s.spawn(move |_| {
for line in stderr_reader.lines() {
match line {
Ok(line) => {
eprintln!("{}", line); let mut captured = captured.lock().unwrap();
captured.push_str(&line);
captured.push('\n');
}
Err(_) => break,
}
}
});
});
if thread_result.is_err() {
return Err(CommandError::ExecutionFailed("Failed to execute threads".into()));
}
let output = captured_output.lock().unwrap().clone();
let success_flag = process.wait().unwrap().success();
Ok((output, success_flag))
}
fn get_systypes_folder_name() -> &'static str {
"systypes"
}
pub fn is_wsl() -> bool {
#[cfg(target_os = "windows")]
{
return false;
}
#[cfg(not(target_os = "windows"))]
{
if env::var("WSL_DISTRO_NAME").is_ok() {
return true;
}
let proc_version = fs::read_to_string("/proc/version");
if proc_version.is_ok() {
return proc_version.as_ref().unwrap().contains("Microsoft") || proc_version.unwrap().contains("WSL");
}
return false;
}
}
pub fn find_executable(executables: &[&str]) -> Option<String> {
for &exe in executables {
if which::which(exe).is_ok() {
return Some(exe.to_string());
}
}
None
}
fn check_python_esptool() -> bool {
Command::new("python")
.args(&["-m", "esptool", "version"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn get_flash_tool_cmd(flash_tool_opt: Option<String>, native_serial_port: bool) -> String {
match flash_tool_opt {
Some(tool) => tool,
None => {
let possible_executables = if cfg!(target_os = "windows") {
vec!["esptool", "esptool.py", "esptool.exe"]
} else if is_wsl() {
if native_serial_port {
vec!["esptool.py", "esptool"]
} else {
vec!["esptool", "esptool.py.exe", "esptool.exe"]
}
} else {
vec!["esptool.py", "esptool"]
};
if let Some(exe) = find_executable(&possible_executables) {
exe
} else if check_python_esptool() {
"python -m esptool".to_string()
} else {
if cfg!(target_os = "windows") {
"esptool".to_string()
} else {
"esptool.py".to_string()
}
}
}
}
}
pub fn get_build_folder_name(sys_type: String, app_folder: String) -> String {
let build_folder_name = format!("{}/build/{}", app_folder, sys_type);
build_folder_name
}
pub fn build_flash_command_args(
build_folder: String,
port: &str,
flash_baud: u32,
skip_fs: bool,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let flash_args_file = format!("{}/flasher_args.json", build_folder);
let flash_args = fs::read_to_string(&flash_args_file)?;
let flash_args: serde_json::Value = serde_json::from_str(&flash_args)?;
let flash_baud = format!("{}", flash_baud);
let flash_mode = flash_args["flash_settings"]["flash_mode"].as_str().unwrap();
let flash_size = flash_args["flash_settings"]["flash_size"].as_str().unwrap();
let flash_freq = flash_args["flash_settings"]["flash_freq"].as_str().unwrap();
let chip_type = flash_args["extra_esptool_args"]["chip"].as_str().unwrap();
let mut esptool_args = vec![
"-p".to_string(),
port.to_string(),
"-b".to_string(),
flash_baud,
"--before".to_string(),
"default_reset".to_string(),
"--after".to_string(),
"hard_reset".to_string(),
"--chip".to_string(),
chip_type.to_string(),
"write_flash".to_string(),
"--flash_mode".to_string(),
flash_mode.to_string(),
"--flash_size".to_string(),
flash_size.to_string(),
"--flash_freq".to_string(),
flash_freq.to_string(),
];
let mut known_offsets: Vec<String> = Vec::new();
for key in &["bootloader", "app", "partition-table", "partition_table"] {
if let Some(offset) = flash_args[key]["offset"].as_str() {
known_offsets.push(offset.to_string());
}
}
if let Some(flash_files) = flash_args["flash_files"].as_object() {
for (offset, file_path) in flash_files {
let file_path_str = file_path.as_str().unwrap();
if skip_fs && !known_offsets.contains(offset) {
let lower = file_path_str.to_lowercase();
let basename = lower.rsplit(|c| c == '/' || c == '\\').next().unwrap_or(&lower);
if basename == "fs.bin"
|| lower.contains("spiffs") || lower.contains("littlefs")
|| lower.contains("fatfs") || lower.contains("storage")
|| lower.contains("fs_image") {
println!("Skipping filesystem image: {}", file_path_str);
continue;
}
}
let full_path = format!("{}/{}", build_folder, file_path_str);
esptool_args.push(offset.clone());
esptool_args.push(full_path);
}
}
Ok(esptool_args)
}
pub fn check_target_folder_valid(target_folder: &str, clean: bool) -> bool {
if !Path::new(&target_folder).exists() {
match std::fs::create_dir(&target_folder) {
Ok(_) => println!("Created folder: {}", target_folder),
Err(e) => {
println!("Error creating folder: {}", e);
return false;
}
}
} else {
if std::fs::read_dir(&target_folder).unwrap().next().is_some() {
if clean {
match remove_dir_contents(&target_folder) {
Ok(_) => println!("Deleted folder contents: {}", target_folder),
Err(e) => {
println!("Error deleting folder contents: {}", e);
return false;
}
}
} else {
println!("Error: target folder must be empty: {}", target_folder);
return false;
}
}
}
true
}
pub fn is_esp_idf_env() -> bool {
env::var("IDF_PATH").is_ok()
}
pub fn idf_version_ok(required_esp_idf_version: String) -> bool {
let idf_output = Command::new("idf.py")
.arg("--version")
.output()
.expect("Failed to run idf.py --version");
println!("idf_version returned from idf.py: {:?}", idf_output);
if !idf_output.status.success() {
println!("Failed to run idf.py --version");
return false;
}
let idf_version_output = String::from_utf8_lossy(&idf_output.stdout);
let idf_version = idf_version_output
.split_whitespace() .nth(1) .unwrap_or("") .trim_start_matches('v') .split('-') .next() .unwrap_or("");
let idf_version_normalized = idf_version.split('.').take(3).collect::<Vec<&str>>().join(".");
let required_version_normalized = required_esp_idf_version.split('.').take(3).collect::<Vec<&str>>().join(".");
println!(
"idf_version_normalized: {:?}, required_version_normalized: {:?}",
idf_version_normalized, required_version_normalized
);
if idf_version_normalized != required_version_normalized {
println!(
"Error: ESP-IDF version mismatch: Required: {}, Found: {}",
required_version_normalized, idf_version_normalized
);
return false;
}
true
}
pub fn is_docker_available() -> bool {
Command::new("docker")
.arg("--version")
.output()
.map_or(false, |output| output.status.success())
}
pub fn get_esp_idf_version_from_dockerfile(dockerfile_path: &str) -> Result<String, Box<dyn std::error::Error>> {
let dockerfile_path = Path::new(dockerfile_path).join("Dockerfile");
let dockerfile_content = fs::read_to_string(dockerfile_path)?;
for line in dockerfile_content.lines() {
if line.starts_with("FROM espressif/idf:") {
let version = line.replace("FROM espressif/idf:", "").trim().to_string();
if version.starts_with('v') {
return Ok(version[1..].to_string());
}
return Ok(version);
}
}
Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"ESP-IDF version not found in Dockerfile",
)))
}
pub fn find_matching_esp_idf(target_version: String, user_path: Option<String>) -> Option<PathBuf> {
if let Some(path) = user_path {
let user_dir = Path::new(&path);
if user_dir.is_dir() {
if user_dir.join("export.sh").is_file() {
println!("Found required ESP IDF folder {:?}", user_dir);
return Some(user_dir.to_path_buf());
}
if let Some(matching_path) = user_dir
.read_dir()
.ok()?
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.find(|p| p.file_name().map_or(false, |name| name.to_string_lossy().ends_with(&target_version)))
{
println!("Found matching path: {:?}", matching_path);
return Some(matching_path);
}
}
}
let default_paths = get_default_esp_idf_paths();
println!("Searching default paths: {:?}", default_paths);
for path in default_paths {
if path.is_dir() {
if let Some(matching_path) = path
.read_dir()
.ok()?
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.find(|p| p.file_name().map_or(false, |name| name.to_string_lossy().ends_with(&target_version)))
{
println!("Found matching path: {:?}", matching_path);
return Some(matching_path);
}
}
}
println!("No matching ESP-IDF found for {:?}", target_version);
None
}
fn get_default_esp_idf_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
#[cfg(target_os = "linux")]
paths.push(dirs::home_dir().unwrap_or_default().join("esp"));
#[cfg(target_os = "windows")]
paths.push(PathBuf::from("C:\\Espressif\\frameworks"));
#[cfg(target_os = "macos")]
paths.push(dirs::home_dir().unwrap_or_default().join("esp"));
paths
}
pub fn prepare_esp_idf(idf_path: &Path) -> Result<HashMap<String, String>, Box<dyn std::error::Error>> {
let mut env_vars = HashMap::new();
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let export_script = idf_path.join("export.sh");
if export_script.exists() {
println!("Capturing ESP-IDF environment from {}", idf_path.display());
let output = Command::new("bash")
.arg("-c")
.arg(format!("source {} && env", export_script.display()))
.stdout(Stdio::piped())
.output()?;
if !output.status.success() {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to capture ESP-IDF environment",
)));
}
for line in String::from_utf8_lossy(&output.stdout).lines() {
if let Some((key, value)) = line.split_once('=') {
env_vars.insert(key.to_string(), value.to_string());
}
}
} else {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"export.sh not found in ESP-IDF folder",
)));
}
}
#[cfg(target_os = "windows")]
{
let export_script = idf_path.join("export.bat");
if export_script.exists() {
println!("Capturing ESP-IDF environment from {}", idf_path.display());
let output = Command::new("cmd")
.args(["/C", export_script.to_str().unwrap(), "&&", "set"])
.stdout(Stdio::piped())
.output()?;
if !output.status.success() {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to capture ESP-IDF environment",
)));
}
for line in String::from_utf8_lossy(&output.stdout).lines() {
if let Some((key, value)) = line.split_once('=') {
env_vars.insert(key.to_string(), value.to_string());
}
}
} else {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"export.bat not found in ESP-IDF folder",
)));
}
}
Ok(env_vars)
}