diff --git a/Cargo.lock b/Cargo.lock index 2db7f66..9183916 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1187,7 +1187,7 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idf-im-lib" -version = "0.1.2" +version = "0.1.3" dependencies = [ "colored", "config", diff --git a/Cargo.toml b/Cargo.toml index 19322ab..e033a1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "idf-im-lib" -version = "0.1.2" +version = "0.1.3" edition = "2021" [features] diff --git a/src/idf_tools.rs b/src/idf_tools.rs index f5d64e4..778c442 100644 --- a/src/idf_tools.rs +++ b/src/idf_tools.rs @@ -1,10 +1,11 @@ use serde::Deserialize; use std::collections::HashMap; -use std::fs::File; +use std::fs::{self, File}; use std::io::prelude::*; -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::python_utils::get_python_platform_definition; +use crate::system_dependencies; #[derive(Deserialize, Debug, Clone)] pub struct Tool { @@ -256,12 +257,187 @@ pub fn change_links_donwanload_mirror( new_tools } +/// Retrieves a HashMap of tool names and their corresponding Download instances based on the given platform. +/// +/// # Parameters +/// +/// * `tools_file`: A `ToolsFile` instance containing the list of tools and their versions. +/// * `selected_chips`: A vector of strings representing the selected chips. +/// * `mirror`: An optional reference to a string representing the mirror URL. If `None`, the original URLs are used. +/// +/// # Return +/// +/// * A HashMap where the keys are tool names and the values are Download instances. +/// If a tool does not have a download for the given platform, it is not included in the HashMap. +/// +pub fn get_list_of_tools_to_download( + tools_file: ToolsFile, + selected_chips: Vec, + mirror: Option<&str>, +) -> HashMap { + let list = filter_tools_by_target(tools_file.tools, &selected_chips); + let platform = match get_platform_identification(None) { + Ok(platform) => platform, + Err(err) => { + if std::env::consts::OS == "windows" { + // All this is for cases when on windows microsoft store creates "pseudolinks" for python + let scp = system_dependencies::get_scoop_path(); + let usable_python = match scp { + Some(path) => { + let mut python_path = PathBuf::from(path); + python_path.push("python3.exe"); + python_path.to_str().unwrap().to_string() + } + None => "python3.exe".to_string(), + }; + match get_platform_identification(Some(&usable_python)) { + Ok(platform) => platform, + Err(err) => { + log::error!("Unable to identify platform: {}", err); + panic!("Unable to identify platform: {}", err); + } + } + } else { + panic!("Unable to identify platform: {}", err); + } + } + }; + change_links_donwanload_mirror(get_download_link_by_platform(list, &platform), mirror) +} + +/// Retrieves a vector of strings representing the export paths for the tools. +/// +/// This function creates export paths for the tools based on their `export_paths` and the `tools_install_path`. +/// It also checks for duplicate export paths and logs them accordingly. +/// +/// # Parameters +/// +/// * `tools_file`: A `ToolsFile` instance containing the list of tools and their versions. +/// * `selected_chip`: A vector of strings representing the selected chips. +/// * `tools_install_path`: A reference to a string representing the installation path for the tools. +/// +/// # Return +/// +/// * A vector of strings representing the export paths for the tools. +/// +pub fn get_tools_export_paths( + tools_file: ToolsFile, + selected_chip: Vec, + tools_install_path: &str, +) -> Vec { + let bin_dirs = find_bin_directories(Path::new(tools_install_path)); + log::debug!("Bin directories: {:?}", bin_dirs); + + let list = filter_tools_by_target(tools_file.tools, &selected_chip); + // debug!("Creating export paths for: {:?}", list); + let mut paths = vec![]; + for tool in &list { + tool.export_paths.iter().for_each(|path| { + let mut p = PathBuf::new(); + p.push(tools_install_path); + for level in path { + p.push(level); + } + paths.push(p.to_str().unwrap().to_string()); + }); + } + for bin_dir in bin_dirs { + let str_p = bin_dir.to_str().unwrap().to_string(); + if paths.contains(&str_p) { + log::trace!("Skipping duplicate export path: {}", str_p); + } else { + log::trace!("Adding export path: {}", str_p); + paths.push(str_p); + } + } + log::debug!("Export paths: {:?}", paths); + paths +} + +/// Recursively searches for directories named "bin" within the given path. +/// +/// # Parameters +/// +/// * `path`: A reference to a `Path` representing the starting directory for the search. +/// +/// # Return +/// +/// * A vector of `PathBuf` instances representing the directories found. +/// +pub fn find_bin_directories(path: &Path) -> Vec { + let mut result = Vec::new(); + + if let Ok(entries) = fs::read_dir(path) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + if path.file_name().and_then(|n| n.to_str()) == Some("bin") { + result.push(path.clone()); + } else { + result.extend(find_bin_directories(&path)); + } + } + } + } + + result +} + #[cfg(test)] mod tests { use std::collections::HashMap; use super::*; + use std::path::Path; + + use super::find_bin_directories; + + #[test] + fn test_find_bin_directories_non_existing_path() { + let non_existing_path = Path::new("/path/that/does/not/exist"); + let result = find_bin_directories(non_existing_path); + + assert_eq!( + result.len(), + 0, + "Expected an empty vector when the path does not exist" + ); + } + #[test] + fn test_find_bin_directories_root_level() { + let test_dir = Path::new("/tmp/test_directory"); + let bin_dir = test_dir.join("bin"); + + // Create the test directory and the "bin" directory + std::fs::create_dir_all(&bin_dir).unwrap(); + + let result = find_bin_directories(&test_dir); + + // Remove the test directory + std::fs::remove_dir_all(&test_dir).unwrap(); + + assert_eq!(result.len(), 1); + assert_eq!(result[0], bin_dir); + } + + #[test] + fn test_find_bin_directories_deeply_nested() { + let test_dir = Path::new("/tmp/test_files/deeply_nested_directory/something/"); + let bin_dir = test_dir.join("bin"); + + // Create the test directory and the "bin" directory + std::fs::create_dir_all(&bin_dir).unwrap(); + + let result = find_bin_directories(&test_dir); + + // Remove the test directory + std::fs::remove_dir_all(&test_dir).unwrap(); + + assert_eq!(result.len(), 1); + assert_eq!(result[0], bin_dir); + } + #[test] fn test_change_links_download_mirror_multiple_tools() { let mut tools = HashMap::new(); diff --git a/src/lib.rs b/src/lib.rs index c6f64f9..bd29eb7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ use std::{ io::{self, Read, Write}, path::{Path, PathBuf}, process::Command, + sync::mpsc::Sender, }; /// Creates an executable shell script with the given content and file path. @@ -378,47 +379,50 @@ pub fn verify_file_checksum(expected_checksum: &str, file_path: &str) -> Result< Ok(computed_checksum == expected_checksum) } -/// Asynchronously downloads a file from a given URL to a specified destination path. +/// Sets up the environment variables required for the ESP-IDF build system. /// -/// # Arguments -/// -/// * `url` - A string representing the URL from which to download the file. -/// * `destination_path` - A string representing the path to which the file should be downloaded. -/// * `show_progress` - A function pointer to a function that will be called to show the progress of the download. -/// -/// # Returns -/// -/// * `Ok(())` if the file was successfully downloaded. -/// * `Err(std::io::Error)` if an error occurred during the download process. -/// -/// # Example +/// # Parameters /// -/// ```rust -/// use std::io; -/// use std::io::Write; -/// use idf_im_lib::download_file; -/// -/// fn download_progress_callback(downloaded: u64, total: u64) { -/// let percentage = (downloaded as f64 / total as f64) * 100.0; -/// print!("\rDownloading... {:.2}%", percentage); -/// io::stdout().flush().unwrap(); -/// } +/// * `tool_install_directory`: A reference to a `PathBuf` representing the directory where the ESP-IDF tools are installed. +/// * `idf_path`: A reference to a `PathBuf` representing the path to the ESP-IDF framework directory. /// -/// #[tokio::main] -/// async fn main() { -/// let url = "https://example.com/file.zip"; -/// let destination_path = "/path/to/destination"; +/// # Return /// -/// match download_file(url, destination_path, &download_progress_callback).await { -/// Ok(()) => println!("\nDownload completed successfully"), -/// Err(e) => eprintln!("Error during download: {}", e), -/// } -/// } -/// ``` +/// * `Result, String>`: +/// - On success, returns a `Vec` of tuples containing the environment variable names and their corresponding values. +/// - On error, returns a `String` describing the error. +/// +pub fn setup_environment_variables( + tool_install_directory: &PathBuf, + idf_path: &PathBuf, +) -> Result, String> { + let mut env_vars = vec![]; + + // env::set_var("IDF_TOOLS_PATH", tool_install_directory); + let instal_dir_string = tool_install_directory.to_str().unwrap().to_string(); + env_vars.push(("IDF_TOOLS_PATH".to_string(), instal_dir_string)); + let idf_path_string = idf_path.to_str().unwrap().to_string(); + env_vars.push(("IDF_PATH".to_string(), idf_path_string)); + + let python_env_path_string = tool_install_directory + .join("python") + .to_str() + .unwrap() + .to_string(); + env_vars.push(("IDF_PYTHON_ENV_PATH".to_string(), python_env_path_string)); + + Ok(env_vars) +} +pub enum DownloadProgress { + Progress(u64, u64), // (downloaded, total) + Complete, + Error(String), +} + pub async fn download_file( url: &str, destination_path: &str, - show_progress: &dyn Fn(u64, u64), + progress_sender: Sender, ) -> Result<(), std::io::Error> { // Create a new HTTP client let client = Client::new(); @@ -432,17 +436,26 @@ pub async fn download_file( // Get the total size of the file being downloaded let total_size = response.content_length().ok_or_else(|| { + let _ = progress_sender.send(DownloadProgress::Error( + "Failed to get content length".into(), + )); std::io::Error::new(std::io::ErrorKind::Other, "Failed to get content length") })?; + log::debug!("Downloading {} to {}", url, destination_path); // Extract the filename from the URL let filename = Path::new(&url).file_name().unwrap().to_str().unwrap(); - + log::debug!( + "Filename: {} and destination: {}", + filename, + destination_path + ); // Create a new file at the specified destination path let mut file = File::create(Path::new(&destination_path).join(Path::new(filename)))?; + log::debug!("Created file at {}", destination_path); // Initialize the amount downloaded - let mut amount_downloaded: u64 = 0; + let mut downloaded: u64 = 0; // Download the file in chunks while let Some(chunk) = response @@ -450,15 +463,22 @@ pub async fn download_file( .await .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { + log::trace!("Downloaded {}/{} bytes", downloaded, total_size); // Update the amount downloaded - amount_downloaded += chunk.len() as u64; + downloaded += chunk.len() as u64; // Write the chunk to the file file.write_all(&chunk)?; // Call the progress callback function - show_progress(amount_downloaded, total_size); + if let Err(e) = progress_sender.send(DownloadProgress::Progress(downloaded, total_size)) { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to send progress: {}", e), + )); + } } + let _ = progress_sender.send(DownloadProgress::Complete); // Return Ok(()) if the download was successful Ok(()) @@ -749,6 +769,21 @@ pub fn run_idf_tools_using_rustpython(custom_path: &str) -> Result` object for sending progress messages. +/// * `with_submodules`: A boolean indicating whether to clone the ESP-IDF repository with submodules. +/// +/// # Return Value +/// +/// * `Result`: On success, returns a `Result` containing the path of the cloned repository as a string. +/// On error, returns a `Result` containing a `git2::Error` indicating the cause of the error. pub fn get_esp_idf_by_version_and_mirror( path: &str, version: &str, @@ -856,6 +891,60 @@ pub fn expand_tilde(path: &Path) -> PathBuf { } } +/// Performs post-installation tasks for a single version of ESP-IDF. +/// +/// This function creates a desktop shortcut on Windows systems and generates an activation shell script +/// for other operating systems. The desktop shortcut is created using the `create_desktop_shortcut` function, +/// and the activation shell script is generated using the `create_activation_shell_script` function. +/// +/// # Parameters +/// +/// * `version_instalation_path`: A reference to a string representing the path where the ESP-IDF version is installed. +/// * `idf_path`: A reference to a string representing the path to the ESP-IDF repository. +/// * `idf_version`: A reference to a string representing the version of ESP-IDF being installed. +/// * `tool_install_directory`: A reference to a string representing the directory where the ESP-IDF tools will be installed. +/// * `export_paths`: A vector of strings representing the paths that need to be exported for the ESP-IDF tools. +pub fn single_version_post_install( + version_instalation_path: &str, + idf_path: &str, + idf_version: &str, + tool_install_directory: &str, + export_paths: Vec, +) { + match std::env::consts::OS { + "windows" => { + // Creating desktop shortcut + if let Err(err) = create_desktop_shortcut( + version_instalation_path, + idf_path, + idf_version, + tool_install_directory, + export_paths, + ) { + error!( + "{} {:?}", + "Failed to create desktop shortcut", + err.to_string() + ) + } else { + info!("Desktop shortcut created successfully") + } + } + _ => { + let install_folder = PathBuf::from(version_instalation_path); + let install_path = install_folder.parent().unwrap().to_str().unwrap(); + let _ = create_activation_shell_script( + // todo: handle error + install_path, + idf_path, + tool_install_directory, + idf_version, + export_paths, + ); + } + } +} + /// Returns a list of available IDF mirrors. /// /// # Purpose diff --git a/src/python_utils.rs b/src/python_utils.rs index 3e56155..934fd6b 100644 --- a/src/python_utils.rs +++ b/src/python_utils.rs @@ -8,7 +8,7 @@ use std::process::ExitCode; #[cfg(feature = "userustpython")] use vm::{builtins::PyStrRef, Interpreter}; -use crate::command_executor; +use crate::{command_executor, replace_unescaped_spaces_posix, replace_unescaped_spaces_win}; /// Runs a Python script from a specified file with optional arguments and environment variables. /// todo: check documentation @@ -83,6 +83,82 @@ pub fn run_python_script_from_file( } } +/// Runs the IDF tools Python installation script. +/// +/// This function prepares the environment to run a Python installation script for +/// IDF tools by ensuring that the path is properly escaped based on the operating +/// system. It then executes the installation script followed by the Python environment +/// setup script. +/// +/// # Parameters +/// +/// - `idf_tools_path`: A string slice that represents the path to the IDF tools. +/// - `environment_variables`: A vector of tuples containing environment variable names +/// and their corresponding values, which will be passed to the installation scripts. +/// +/// # Returns +/// +/// This function returns a `Result`. On success, it returns an `Ok` +/// containing the output of the Python environment setup script. On failure, it returns +/// an `Err` containing an error message. +/// +/// # Example +/// +/// ```rust +/// let path = "path/to/idf_tools"; +/// let env_vars = vec![("VAR_NAME".to_string(), "value".to_string())]; +/// match run_idf_tools_py(path, &env_vars) { +/// Ok(output) => println!("Success: {}", output), +/// Err(e) => eprintln!("Error: {}", e), +/// } +/// ``` + +pub fn run_idf_tools_py( + // todo: rewrite functionality to rust + idf_tools_path: &str, + environment_variables: &Vec<(String, String)>, +) -> Result { + let escaped_path = if std::env::consts::OS == "windows" { + replace_unescaped_spaces_win(&idf_tools_path) + } else { + replace_unescaped_spaces_posix(&idf_tools_path) + }; + run_install_script(&escaped_path, environment_variables)?; + run_install_python_env_script(&escaped_path, environment_variables) +} + +fn run_install_script( + idf_tools_path: &str, + environment_variables: &Vec<(String, String)>, +) -> Result { + let output = run_python_script_from_file( + idf_tools_path, + Some("install"), + None, + Some(environment_variables), + ); + + trace!("idf_tools.py install output:\n{:?}", output); + + output +} + +fn run_install_python_env_script( + idf_tools_path: &str, + environment_variables: &Vec<(String, String)>, +) -> Result { + let output = run_python_script_from_file( + idf_tools_path, + Some("install-python-env"), + None, + Some(environment_variables), + ); + + trace!("idf_tools.py install-python-env output:\n{:?}", output); + + output +} + /// Executes a Python script using the provided Python interpreter and returns the script's output. /// /// # Parameters diff --git a/src/settings.rs b/src/settings.rs index 3c23c77..83ca709 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -4,10 +4,10 @@ use std::fs::OpenOptions; use std::io::Write; use std::path::PathBuf; -#[derive(Debug, Deserialize, Default, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct Settings { pub path: Option, - pub idf_path: Option, + pub idf_path: Option, // TOOD: These are actually multiple because of multiple version --> remove from config alltogether or changed it to computed property pub tool_download_folder_name: Option, pub tool_install_folder_name: Option, pub target: Option>, @@ -22,6 +22,27 @@ pub struct Settings { pub recurse_submodules: Option, } +impl Default for Settings { + fn default() -> Self { + Self { + path: None, + idf_path: None, + tool_download_folder_name: Some("dist".to_string()), + tool_install_folder_name: Some("tools".to_string()), + target: None, + idf_versions: None, + tools_json_file: Some("tools/tools.json".to_string()), + idf_tools_path: Some("tools/idf_tools.py".to_string()), + config_file: None, + non_interactive: Some(false), + wizard_all_questions: Some(false), + mirror: None, + idf_mirror: None, + recurse_submodules: Some(false), + } + } +} + impl Settings { pub fn new( config_path: Option,