Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ required-features = ["offline"]
tauri-build = { version = "2.3.1", features = [], optional = true }

[dependencies]
reqwest = "0.12.4"
reqwest = { version = "0.12.4", features = ["stream", "json"] }
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
serde_json = "1.0"
Expand Down Expand Up @@ -82,6 +82,7 @@ once_cell = "1.21.3"
idf-env = { git = "https://github.com/espressif/idf-env", rev="fd69ab4f550ef35647bb32d1584caa6623cbfc4e" }
fs_extra = { version = "1.3.0", optional = true }
lnk = "0.6.3"
url = "2.5.7"


# GUI-related dependencies (optional)
Expand Down
3 changes: 2 additions & 1 deletion src-tauri/src/cli/cli_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,8 @@ impl IntoIterator for InstallArgs {
"tools_json_file".to_string(),
self.tools_json_file.map(Into::into),
),
("mirror".to_string(), self.mirror.map(Into::into)),
// map CLI flag `-m|--mirror` to settings field `tools_mirror`
("tools_mirror".to_string(), self.mirror.map(Into::into)),
("idf_mirror".to_string(), self.idf_mirror.map(Into::into)),
("pypi_mirror".to_string(), self.pypi_mirror.map(Into::into)),
(
Expand Down
35 changes: 35 additions & 0 deletions src-tauri/src/cli/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ use std::{
fmt::Write,
time::{Duration, Instant},
};
use idf_im_lib::utils::{calculate_mirror_latency_map};

/// A tuple containing a mirror URL and its measured latency.
pub type MirrorEntry = (String, Option<u32>);

pub fn run_with_spinner<F, T>(func: F) -> T
where
Expand Down Expand Up @@ -140,3 +144,34 @@ pub async fn track_cli_event(event_name: &str, additional_data: Option<serde_jso
"additional_data": additional_data
})).await;
}

/// Sort mirrors by measured latency (ascending), using None for timeouts.
pub async fn sorted_mirror_entries(mirrors: &[&str]) -> Vec<MirrorEntry> {
let latency_map = calculate_mirror_latency_map(&mirrors.to_vec()).await;
let mut entries: Vec<MirrorEntry> = Vec::new();
for (key, value) in latency_map.iter() {
entries.push((key.clone(), value.clone()));
}

entries.sort_by_key(|e| e.1);
entries
}

/// Turn `(url, latency)` tuples into display strings like `https://... (123 ms)` or `(... timeout)`.
pub fn mirror_entries_to_display(entries: &[MirrorEntry]) -> Vec<String> {
entries
.iter()
.map(|(u, s)| {
if s.is_none() {
format!("{u} (timeout)")
} else {
format!("{u} ({:?} ms)", s.unwrap())
}
})
.collect()
}

/// Strip the latency suffix back to a plain URL.
pub fn url_from_display_line(selected: &str) -> String {
selected.split(" (").next().unwrap_or(selected).to_string()
}
122 changes: 83 additions & 39 deletions src-tauri/src/cli/prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ use log::{debug, info};
use rust_i18n::t;

use crate::cli::helpers::generic_confirm_with_default;
use crate::cli::helpers::sorted_mirror_entries;
use crate::cli::helpers::mirror_entries_to_display;
use crate::cli::helpers::url_from_display_line;

pub async fn select_target() -> Result<Vec<String>, String> {
let mut available_targets = idf_im_lib::idf_versions::get_avalible_targets().await?;
Expand Down Expand Up @@ -176,48 +179,89 @@ pub fn check_and_install_python(
Ok(())
}

pub fn select_mirrors(mut config: Settings) -> Result<Settings, String> {
if (config.wizard_all_questions.unwrap_or_default()
|| config.idf_mirror.is_none()
|| config.is_default("idf_mirror"))
&& config.non_interactive == Some(false)
{
config.idf_mirror = Some(generic_select(
"wizard.idf.mirror",
&idf_im_lib::get_idf_mirrors_list()
.iter()
.map(|&s| s.to_string())
.collect(),
)?)
}
async fn select_single_mirror<FGet, FSet>(
config: &mut Settings,
field_name: &str, // e.g. "idf_mirror"
get_value: FGet, // e.g. |c: &Settings| &c.idf_mirror
set_value: FSet, // e.g. |c: &mut Settings, v| c.idf_mirror = Some(v)
candidates: &[&str], // list of mirror URLs
wizard_key: &str, // e.g. "wizard.idf.mirror"
log_prefix: &str, // e.g. "IDF", "Tools", "PyPI"
) -> Result<(), String>
where
FGet: Fn(&Settings) -> &Option<String>,
FSet: Fn(&mut Settings, String),
{
// Interactive by default when non_interactive is None
let interactive = !config.non_interactive.unwrap_or_default();
let wizard_all = config.wizard_all_questions.unwrap_or_default();
let current = get_value(config);
let needs_value = current.is_none() || config.is_default(field_name);

if (config.wizard_all_questions.unwrap_or_default()
|| config.mirror.is_none()
|| config.is_default("mirror"))
&& config.non_interactive == Some(false)
{
config.mirror = Some(generic_select(
"wizard.tools.mirror",
&idf_im_lib::get_idf_tools_mirrors_list()
.iter()
.map(|&s| s.to_string())
.collect(),
)?)
// Only measure mirror latency if we actually need a value (or wizard wants to ask)
if interactive && (wizard_all || needs_value) {
let entries = sorted_mirror_entries(candidates).await;
let display = mirror_entries_to_display(&entries);
let selected = generic_select(wizard_key, &display)?;
let url = url_from_display_line(&selected);
set_value(config, url);
} else if needs_value {
let entries = sorted_mirror_entries(candidates).await;
if let Some((url, score)) = entries.first() {
if score.is_none() {
info!("Selected {log_prefix} mirror: {url} (timeout)");
} else {
info!("Selected {log_prefix} mirror: {url} ({:?} ms)", score.unwrap());
}
set_value(config, url.clone());
}
}

if (config.wizard_all_questions.unwrap_or_default()
|| config.pypi_mirror.is_none()
|| config.is_default("pypi_mirror"))
&& config.non_interactive == Some(false)
{
config.pypi_mirror = Some(generic_select(
"wizard.pypi.mirror",
&idf_im_lib::get_pypi_mirrors_list()
.iter()
.map(|&s| s.to_string())
.collect(),
)?)
}
Ok(())
}

pub async fn select_mirrors(mut config: Settings) -> Result<Settings, String> {
// IDF mirror
let idf_candidates = idf_im_lib::get_idf_mirrors_list();

select_single_mirror(
&mut config,
"idf_mirror",
|c: &Settings| &c.idf_mirror,
|c: &mut Settings, v| c.idf_mirror = Some(v),
idf_candidates,
"wizard.idf.mirror",
"IDF",
)
.await?;

// Tools mirror
let tools_candidates = idf_im_lib::get_idf_tools_mirrors_list();

select_single_mirror(
&mut config,
"tools_mirror",
|c: &Settings| &c.tools_mirror,
|c: &mut Settings, v| c.tools_mirror = Some(v),
tools_candidates,
"wizard.tools.mirror",
"Tools",
)
.await?;

// PyPI mirror
let pypi_candidates = idf_im_lib::get_pypi_mirrors_list();

select_single_mirror(
&mut config,
"pypi_mirror",
|c: &Settings| &c.pypi_mirror,
|c: &mut Settings, v| c.pypi_mirror = Some(v),
pypi_candidates,
"wizard.pypi.mirror",
"PyPI",
)
.await?;

Ok(config)
}
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/cli/wizard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ async fn download_and_extract_tools(
config.target.clone().unwrap(),
download_dir,
install_dir,
config.mirror.as_deref(),
config.tools_mirror.as_deref(),
progress_callback,
)
.await
Expand Down Expand Up @@ -396,7 +396,7 @@ pub async fn run_wizzard_run(mut config: Settings) -> Result<(), String> {
config = select_targets_and_versions(config).await?;

// mirrors select
config = select_mirrors(config)?;
config = select_mirrors(config).await?;

config = select_installation_path(config)?;

Expand Down
23 changes: 23 additions & 0 deletions src-tauri/src/gui/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,29 @@ pub struct AppState {
wizard_data: Mutex<WizardData>,
settings: Mutex<Settings>,
is_installing: Mutex<bool>,
is_simple_installation: Mutex<bool>,
}

pub fn set_is_simple_installation(app_handle: &AppHandle, is_simple: bool) -> Result<(), String> {
let app_state = app_handle.state::<AppState>();
let mut simple_installation = app_state
.is_simple_installation
.lock()
.map_err(|_| "Lock error".to_string())?;
*simple_installation = is_simple;
Ok(())
}

pub fn is_simple_installation(app_handle: &AppHandle) -> bool {
let app_state = app_handle.state::<AppState>();
app_state
.is_simple_installation
.lock()
.map(|guard| *guard)
.unwrap_or_else(|_| {
error!("Failed to acquire is_simple_installation lock, assuming false");
false
})
}

/// Gets the current settings from the app state
Expand Down
27 changes: 24 additions & 3 deletions src-tauri/src/gui/commands/idf_tools.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::gui::ui::{emit_installation_event, emit_log_message, send_message, send_tools_message, InstallationProgress, InstallationStage, MessageLevel, ProgressBar};
use crate::gui::{ui::{InstallationProgress, InstallationStage, MessageLevel, ProgressBar, emit_installation_event, emit_log_message, send_message, send_tools_message}, utils::get_best_mirror};
use anyhow::{anyhow, Context, Result};

use idf_im_lib::{
Expand Down Expand Up @@ -95,6 +95,7 @@ pub async fn setup_tools(
offline_archive_dir: Option<&Path>,
) -> Result<Vec<String>> {
info!("Setting up tools...");
let is_simple_installation = crate::gui::app_state::is_simple_installation(&app_handle);

let version_path = idf_path
.parent()
Expand Down Expand Up @@ -312,13 +313,23 @@ pub async fn setup_tools(
}
};

let tools_mirror = settings.tools_mirror.as_deref().unwrap();
let mut tools_mirror_to_use: Option<String> = Some(tools_mirror.to_string());
if is_simple_installation && settings.is_default("tools_mirror") {
let mirror_latency_map = idf_im_lib::utils::calculate_mirror_latency_map(&idf_im_lib::get_idf_tools_mirrors_list().to_vec()).await;
let best_mirror = get_best_mirror(&mirror_latency_map).await;
if best_mirror.is_some() {
tools_mirror_to_use = Some(best_mirror.unwrap());
}
}

// Use the library's setup_tools function
let installed_tools_list = idf_tools::setup_tools(
&tools,
settings.target.clone().unwrap_or_default(),
&PathBuf::from(&tool_setup.download_dir),
&PathBuf::from(&tool_setup.install_dir),
settings.mirror.as_deref(),
tools_mirror_to_use.as_deref(),
progress_callback,
)
.await
Expand Down Expand Up @@ -353,6 +364,16 @@ pub async fn setup_tools(
}
};

let pypi_mirror = settings.pypi_mirror.as_deref().unwrap();
let mut pypi_mirror_to_use: Option<String> = Some(pypi_mirror.to_string());
if is_simple_installation && settings.is_default("pypi_mirror") {
let pypi_mirror_latency_map = idf_im_lib::utils::calculate_mirror_latency_map(&idf_im_lib::get_pypi_mirrors_list().to_vec()).await;
let best_pypi_mirror = get_best_mirror(&pypi_mirror_latency_map).await;
if best_pypi_mirror.is_some() {
pypi_mirror_to_use = Some(best_pypi_mirror.unwrap());
}
}

// Install Python environment
match idf_im_lib::python_utils::install_python_env(
&paths,
Expand All @@ -361,7 +382,7 @@ pub async fn setup_tools(
true, //TODO: actually read from config
&settings.idf_features.clone().unwrap_or_default(),
offline_archive_dir, // Offline archive directory
&settings.pypi_mirror, // PyPI mirror
&pypi_mirror_to_use, // PyPI mirror
).await {
Ok(_) => {
info!("Python environment installed");
Expand Down
Loading
Loading