[BugFix] ODP Desktop: Fix Dependabot Alerts, Fix Environment Update, Improve Error Message (#7345)

* improve error message on initial environment setup before continue anyway, add openbb-cookiecutter to the extra extensions lists, seperate conda env update into updating conda packages and pip packages, update glob for dependabot

* conda update -> conda install for updating the individual packages

* cargo fmt

* cargo clippy
This commit is contained in:
Danglewood
2026-02-06 16:22:31 -08:00
committed by GitHub
parent ed414415bd
commit 117dc73c86
13 changed files with 804 additions and 793 deletions

1040
desktop/Cargo.lock generated vendored

File diff suppressed because it is too large Load Diff

71
desktop/package-lock.json generated vendored
View File

@@ -16,17 +16,17 @@
"@tanstack/router-core": "^1.114.33",
"@tanstack/router-devtools": "^1.131.27",
"@tauri-apps/plugin-app": "^2.0.0-alpha.1",
"@tauri-apps/plugin-dialog": "^2.3.3",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-http": "^2.5.2",
"@tauri-apps/plugin-log": "^2.6.0",
"@tauri-apps/plugin-log": "^2.8.0",
"@tauri-apps/plugin-opener": "^2.5.0",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-updater": "^2.9.0",
"@tauri-apps/plugin-updater": "^2.9.6",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"date-fns": "^4.1.0",
"glob": ">=10.5.0",
"glob": ">=13.0.1",
"postcss": "^8.5.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -42,7 +42,7 @@
"devDependencies": {
"@eslint/js": "^9.39.1",
"@tanstack/router-vite-plugin": "^1.131.27",
"@tauri-apps/api": "^2.8.0",
"@tauri-apps/api": "^2.9.6",
"@tauri-apps/cli": "^2.9.6",
"@tauri-apps/plugin-shell": "^2.3.1",
"@tauri-apps/plugin-window": "^2.0.0-alpha.1",
@@ -55,6 +55,7 @@
"@typescript-eslint/parser": "^8.37.0",
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.4.21",
"baseline-browser-mapping": "^2.9.19",
"eslint": "^9.33.0",
"eslint-plugin-react": "^7.37.5",
"jsdom": "^26.1.0",
@@ -1536,9 +1537,9 @@
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
@@ -3462,9 +3463,9 @@
}
},
"node_modules/@tauri-apps/api": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.8.0.tgz",
"integrity": "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==",
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
"integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
@@ -3713,9 +3714,9 @@
}
},
"node_modules/@tauri-apps/plugin-dialog": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.4.0.tgz",
"integrity": "sha512-OvXkrEBfWwtd8tzVCEXIvRfNEX87qs2jv6SqmVPiHcJjBhSF/GUvjqUNIDmKByb5N8nvDqVUM7+g1sXwdC/S9w==",
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz",
"integrity": "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.8.0"
@@ -3740,9 +3741,9 @@
}
},
"node_modules/@tauri-apps/plugin-log": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.7.0.tgz",
"integrity": "sha512-81XQ2f93x4vmIB5OY0XlYAxy60cHdYLs0Ki8Qp38tNATRiuBit+Orh3frpY3qfYQnqEvYVyRub7YRJWlmW2RRA==",
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.8.0.tgz",
"integrity": "sha512-a+7rOq3MJwpTOLLKbL8d0qGZ85hgHw5pNOWusA9o3cf7cEgtYHiGY/+O8fj8MvywQIGqFv0da2bYQDlrqLE7rw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.8.0"
@@ -3777,12 +3778,12 @@
}
},
"node_modules/@tauri-apps/plugin-updater": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.9.0.tgz",
"integrity": "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.0.tgz",
"integrity": "sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.6.0"
"@tauri-apps/api": "^2.10.1"
}
},
"node_modules/@tauri-apps/plugin-window": {
@@ -4897,9 +4898,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.12",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.12.tgz",
"integrity": "sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==",
"version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -5562,9 +5563,9 @@
"license": "Apache-2.0"
},
"node_modules/diff": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
"integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==",
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz",
"integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -6582,12 +6583,12 @@
}
},
"node_modules/glob": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
"integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz",
"integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==",
"license": "BlueOak-1.0.0",
"dependencies": {
"minimatch": "^10.1.1",
"minimatch": "^10.1.2",
"minipass": "^7.1.2",
"path-scurry": "^2.0.0"
},
@@ -6611,12 +6612,12 @@
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz",
"integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"@isaacs/brace-expansion": "^5.0.1"
},
"engines": {
"node": "20 || >=22"

15
desktop/package.json vendored
View File

@@ -1,7 +1,7 @@
{
"name": "openbb-platform",
"private": true,
"version": "1.0.0",
"version": "1.0.1",
"type": "module",
"license": "AGPL-3.0",
"scripts": {
@@ -21,17 +21,17 @@
"@tanstack/router-core": "^1.114.33",
"@tanstack/router-devtools": "^1.131.27",
"@tauri-apps/plugin-app": "^2.0.0-alpha.1",
"@tauri-apps/plugin-dialog": "^2.3.3",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-http": "^2.5.2",
"@tauri-apps/plugin-log": "^2.6.0",
"@tauri-apps/plugin-log": "^2.8.0",
"@tauri-apps/plugin-opener": "^2.5.0",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-updater": "^2.9.0",
"@tauri-apps/plugin-updater": "^2.9.6",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"date-fns": "^4.1.0",
"glob": ">=10.5.0",
"glob": ">=13.0.1",
"postcss": "^8.5.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -47,10 +47,10 @@
"devDependencies": {
"@eslint/js": "^9.39.1",
"@tanstack/router-vite-plugin": "^1.131.27",
"@tauri-apps/api": "^2.8.0",
"@tauri-apps/api": "^2.9.6",
"@tauri-apps/cli": "^2.9.6",
"@tauri-apps/plugin-shell": "^2.3.1",
"@tauri-apps/plugin-window": "^2.0.0-alpha.1",
"@tauri-apps/cli": "^2.9.6",
"@testing-library/jest-dom": "^6.7.0",
"@testing-library/react": "^16.3.0",
"@types/node": "^20.19.11",
@@ -60,6 +60,7 @@
"@typescript-eslint/parser": "^8.37.0",
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.4.21",
"baseline-browser-mapping": "^2.9.19",
"eslint": "^9.33.0",
"eslint-plugin-react": "^7.37.5",
"jsdom": "^26.1.0",

View File

@@ -1,6 +1,6 @@
[package]
name = "openbb-platform"
version = "1.0.0"
version = "1.0.1"
description = "Open Data Platform by OpenBB. A desktop application for managing virtual environments, application backend servers."
authors = ["OpenBB, Inc."]
license = "AGPL-3.0"
@@ -20,9 +20,9 @@ serde_yaml = "^0.9"
chrono = "^0.4"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.8.2", features = ["tray-icon", "devtools"] }
tauri = { version = "2.9.6", features = ["tray-icon", "devtools"] }
tauri-cli = "^2.9.6"
tauri-plugin-log = "2.6.0"
tauri-plugin-log = "2.8.0"
regex = "^1.11.1"
reqwest = { version = "^0.12.23", features = ["json"] }
once_cell = "^1.21.3"

View File

@@ -19,8 +19,8 @@ use tauri_plugin_dialog::DialogExt;
extern crate winapi;
use crate::tauri_handlers::startup::{
abort_installation, get_installation_status, install_conda, install_to_directory,
setup_python_environment,
abort_installation, create_default_backend_services, get_installation_status, install_conda,
install_to_directory, setup_python_environment,
};
use crate::tauri_handlers::environments::{
@@ -538,7 +538,8 @@ fn main() {
uninstall_application,
quit_application,
generate_self_signed_cert,
update_openbb_settings
update_openbb_settings,
create_default_backend_services
])
.setup(|app_handle| {
let install_state = check_installation_on_startup();

View File

@@ -2303,16 +2303,16 @@ pub async fn update_extension_impl<F: FileSystem, E: EnvSystem>(
conda_dir.join("bin").join("conda")
};
let conda_args = if environment == "base" {
vec!["update", &package, "-y"]
vec!["install", &package, "-y"]
} else {
vec!["update", "-n", &environment, &package, "-y"]
vec!["install", "-n", &environment, &package, "-y"]
};
let mut conda_command = env_sys.new_conda_command(&conda_exe, &conda_dir);
let conda_output = conda_command
.args(&conda_args)
.output()
.map_err(|e| format!("Failed to update extension with conda: {e}"))?;
.map_err(|e| format!("Failed to install extension with conda: {e}"))?;
if conda_output.status.success() {
log::debug!("Successfully updated extension '{package}' with conda");
Ok(true)
@@ -2777,7 +2777,7 @@ pub async fn update_environment_impl<F: FileSystem, E: EnvSystem>(
) -> Result<bool, String> {
use std::path::Path;
log::debug!("Updating packages in environment: {environment}");
log::info!("Updating packages in environment: {environment}");
// Path to conda
let conda_dir = Path::new(&directory).join("conda");
@@ -2790,87 +2790,160 @@ pub async fn update_environment_impl<F: FileSystem, E: EnvSystem>(
return Err(format!("Environment YAML file not found for {environment}"));
}
// Read and parse YAML to check for 'openbb'
// Read and parse YAML to extract packages
let yaml_content = fs
.read_to_string(&yaml_path)
.map_err(|e| format!("Failed to read environment YAML: {e}"))?;
let yaml_value: serde_yaml::Value = serde_yaml::from_str(&yaml_content)
.map_err(|e| format!("Failed to parse environment YAML: {e}"))?;
let mut has_openbb = false;
// Extract conda and pip packages from YAML
let mut conda_packages: Vec<String> = Vec::new();
let mut pip_packages: Vec<String> = Vec::new();
if let Some(deps) = yaml_value.get("dependencies").and_then(|d| d.as_sequence()) {
for dep in deps {
// Check if it's a pip mapping
if let Some(pip_map) = dep.as_mapping()
&& let Some(pip_deps) = pip_map
.get(serde_yaml::Value::String("pip".to_string()))
.and_then(|p| p.as_sequence())
{
for pip_dep in pip_deps {
if let Some(pip_dep_str) = pip_dep.as_str()
&& pip_dep_str == "openbb"
{
has_openbb = true;
break;
if let Some(pip_dep_str) = pip_dep.as_str() {
pip_packages.push(pip_dep_str.to_string());
}
}
}
if has_openbb {
break;
} else if let Some(conda_dep) = dep.as_str() {
// It's a conda package (string entry)
// Extract just the package name (remove version specifiers like =3.11)
let pkg_name = conda_dep
.split(['=', '>', '<', '!'])
.next()
.unwrap_or(conda_dep);
// Skip infrastructure packages - we don't want to upgrade these
if !matches!(pkg_name, "python" | "pip" | "nodejs" | "setuptools") {
conda_packages.push(pkg_name.to_string());
}
}
}
}
log::debug!("Using environment definition from: {}", yaml_path.display()); // Get conda executable path
log::info!(
"Found {} conda packages and {} pip packages to update",
conda_packages.len(),
pip_packages.len()
);
if conda_packages.is_empty() && pip_packages.is_empty() {
log::info!("No packages found in environment YAML, nothing to update");
return Ok(true);
}
// Get conda executable
let conda_exe = if env_sys.consts_os() == "windows" {
conda_dir.join("Scripts").join("conda.exe")
} else {
conda_dir.join("bin").join("conda")
};
// Update conda itself first using direct conda command
log::debug!("Updating conda itself");
let mut conda_command = env_sys.new_conda_command(&conda_exe, &conda_dir);
let conda_output = conda_command
.args(["update", "-n", "base", "-c", "conda-forge", "conda", "-y"])
.output()
.map_err(|e| format!("Failed to update conda: {e}"))?;
if !conda_output.status.success() {
log::debug!(
"Warning: Conda self-update may have failed, but continuing with environment update"
// Update conda packages if any (excluding python, pip)
if !conda_packages.is_empty() {
log::info!(
"Updating {} conda packages: {:?}",
conda_packages.len(),
conda_packages
);
} // Use direct conda command instead of scripts that require activation
log::debug!("Updating environment using conda's dependency solver");
let mut update_command = env_sys.new_conda_command(&conda_exe, &conda_dir);
let update_output = update_command
.args([
"env",
"update",
"-n",
&environment,
"-f",
&yaml_path.to_string_lossy(),
"--prune",
])
.output()
.map_err(|e| format!("Failed to update environment: {e}"))?;
let mut conda_args = vec!["install", "-n", &environment, "-y"];
let pkg_refs: Vec<&str> = conda_packages.iter().map(|s| s.as_str()).collect();
conda_args.extend(pkg_refs);
// Handle output
let stdout = String::from_utf8_lossy(&update_output.stdout).to_string();
let stderr = String::from_utf8_lossy(&update_output.stderr).to_string();
log::info!("Running: {} {}", conda_exe.display(), conda_args.join(" "));
log::debug!("Update output: {stdout}");
// Use spawn with timeout to prevent hanging forever
let mut conda_command = env_sys.new_conda_command(&conda_exe, &conda_dir);
let mut child = conda_command
.args(&conda_args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to spawn conda install: {e}"))?;
if !update_output.status.success() {
return Err(format!("Failed to update environment: {stderr}"));
// Run the wait in a blocking thread to not block the async runtime
let result = tokio::task::spawn_blocking(move || {
let timeout = std::time::Duration::from_secs(300);
let start = std::time::Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) => {
// Process finished
let stdout = child
.stdout
.take()
.map(|mut s| {
let mut buf = String::new();
std::io::Read::read_to_string(&mut s, &mut buf).ok();
buf
})
.unwrap_or_default();
let stderr = child
.stderr
.take()
.map(|mut s| {
let mut buf = String::new();
std::io::Read::read_to_string(&mut s, &mut buf).ok();
buf
})
.unwrap_or_default();
return (Some(status), stdout, stderr);
}
Ok(None) => {
// Still running
if start.elapsed() > timeout {
log::warn!("Conda update timed out after 5 minutes, killing process");
let _ = child.kill();
let _ = child.wait();
return (None, String::new(), "Timed out".to_string());
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
Err(e) => {
return (None, String::new(), format!("Error: {e}"));
}
}
}
})
.await
.unwrap_or((None, String::new(), "Task panicked".to_string()));
let (status, stdout, stderr) = result;
log::info!("conda stdout: {}", stdout);
if !stderr.is_empty() {
log::info!("conda stderr: {}", stderr);
}
if let Some(s) = status
&& !s.success()
{
log::warn!(
"Conda update had issues: {}",
if stderr.is_empty() { &stdout } else { &stderr }
);
}
}
// If 'openbb' is in the environment, reinstall it with --no-deps
if has_openbb {
log::debug!("'openbb' package found, running pip install --no-deps");
// Update pip packages
if !pip_packages.is_empty() {
log::info!(
"Updating {} pip packages: {:?}",
pip_packages.len(),
pip_packages
);
let env_python_path = if env_sys.consts_os() == "windows" {
// Get python executable path for this environment
let env_python = if env_sys.consts_os() == "windows" {
conda_dir.join("envs").join(&environment).join("python.exe")
} else {
conda_dir
@@ -2880,23 +2953,86 @@ pub async fn update_environment_impl<F: FileSystem, E: EnvSystem>(
.join("python")
};
let mut pip_command = env_sys.new_conda_command(&env_python_path, &conda_dir);
let pip_output = pip_command
.args(["-m", "pip", "install", "openbb", "--no-deps"])
.output()
.map_err(|e| format!("Failed to run pip install for openbb: {e}"))?;
if !pip_output.status.success() {
let pip_stderr = String::from_utf8_lossy(&pip_output.stderr);
if !fs.exists(&env_python) {
return Err(format!(
"Failed to update the openbb meta package, but all other extensions successfully updated -> {}",
pip_stderr
"Python executable not found for environment {}: {}",
environment,
env_python.display()
));
}
let mut pip_command = env_sys.new_conda_command(&env_python, &conda_dir);
let mut args = vec!["-m", "pip", "install", "--upgrade"];
let package_refs: Vec<&str> = pip_packages.iter().map(|s| s.as_str()).collect();
args.extend(package_refs);
log::info!("Running: {} {}", env_python.display(), args.join(" "));
let output = pip_command
.args(&args)
.stdin(std::process::Stdio::null())
.output()
.map_err(|e| format!("Failed to run pip upgrade: {e}"))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
log::info!("pip stdout: {}", stdout);
if !stderr.is_empty() {
log::info!("pip stderr: {}", stderr);
}
if !output.status.success() {
return Err(format!(
"Failed to update pip packages: {}",
if stderr.is_empty() {
stdout.to_string()
} else {
stderr.to_string()
}
));
}
log::debug!("Successfully re-installed openbb with --no-deps");
}
log::debug!("Successfully updated environment: {environment}");
// Rebuild OpenBB if it's in the environment
if pip_packages
.iter()
.any(|p| p == "openbb" || p.starts_with("openbb-"))
{
log::info!("OpenBB packages detected, running openbb-build");
let openbb_build = if env_sys.consts_os() == "windows" {
conda_dir
.join("envs")
.join(&environment)
.join("Scripts")
.join("openbb-build.exe")
} else {
conda_dir
.join("envs")
.join(&environment)
.join("bin")
.join("openbb-build")
};
if fs.exists(&openbb_build) {
let mut build_command = env_sys.new_conda_command(&openbb_build, &conda_dir);
let build_output = build_command
.stdin(std::process::Stdio::null())
.output()
.map_err(|e| format!("Failed to run openbb-build: {e}"))?;
if !build_output.status.success() {
let build_stderr = String::from_utf8_lossy(&build_output.stderr);
log::warn!("openbb-build had issues: {}", build_stderr);
// Don't fail the whole update if openbb-build has issues
} else {
log::info!("openbb-build completed successfully");
}
}
}
log::info!("Successfully updated environment: {environment}");
Ok(true)
}

View File

@@ -1,6 +1,6 @@
use crate::tauri_handlers::backends::create_backend_service_impl;
use crate::tauri_handlers::helpers::{
EnvSystem, FileExtTrait, FileSystem, RealEnvSystem, RealFileExtTrait, RealFileSystem,
EnvSystem, FileExtTrait, FileSystem, RealEnvSystem, RealFileSystem,
};
use once_cell::sync::Lazy;
use reqwest;
@@ -1237,19 +1237,17 @@ pub async fn setup_python_environment(
window,
&RealFileSystem,
&RealEnvSystem,
&RealFileExtTrait,
)
.await
}
// Split the large function into a separate implementation
async fn setup_python_environment_impl<F: FileSystem, E: EnvSystem, FE: FileExtTrait>(
async fn setup_python_environment_impl<F: FileSystem, E: EnvSystem>(
directory: String,
python_version: String,
window: Window,
fs: &F,
env_sys: &E,
file_ext: &FE,
) -> Result<bool, String> {
use std::path::Path;
@@ -1291,8 +1289,6 @@ async fn setup_python_environment_impl<F: FileSystem, E: EnvSystem, FE: FileExtT
let yaml_path = generate_environment_yaml(&python_version, fs, env_sys).await?;
create_environment_from_yaml(&conda_exe, &yaml_path, &report_progress, env_sys).await?;
install_openbb_package(&conda_path, &report_progress, env_sys).await?;
// Update OpenBB settings
if let Err(e) = crate::tauri_handlers::helpers::update_openbb_settings_impl(
&conda_path,
@@ -1307,9 +1303,6 @@ async fn setup_python_environment_impl<F: FileSystem, E: EnvSystem, FE: FileExtT
report_progress("complete", 1.0, "Installation complete");
// Create default backend service
create_default_backend_service(fs, env_sys, file_ext).await;
window
.emit("installation-directory", &directory)
.unwrap_or_else(|e| {
@@ -1433,71 +1426,19 @@ where
Ok(())
}
async fn install_openbb_package<F, E: EnvSystem>(
conda_path: &Path,
report_progress: &F,
env_sys: &E,
) -> Result<(), String>
where
F: Fn(&str, f32, &str),
{
report_progress("config", 0.85, "Installing OpenBB package");
let env_name = "openbb";
let env_path = conda_path.join("envs").join(env_name);
if !env_path.exists() {
return Err(format!(
"Environment '{}' does not exist at path: {}",
env_name,
env_path.display()
));
}
let conda_exe = if env_sys.consts_os() == "windows" {
conda_path.join("Scripts").join("conda.exe")
} else {
conda_path.join("bin").join("conda")
};
let mut pip_command = env_sys.new_conda_command(&conda_exe, conda_path);
let pip_output = pip_command
.args([
"run",
"-n",
env_name,
"pip",
"install",
"openbb",
"--no-deps",
])
.output()
.map_err(|e| format!("Failed to install openbb package: {e}"))?;
if !pip_output.status.success() {
let stderr = String::from_utf8_lossy(&pip_output.stderr);
return Err(format!("Failed to install openbb package: {stderr}"));
}
let mut build_command = env_sys.new_conda_command(&conda_exe, conda_path);
let build_output = build_command
.args(["run", "-n", env_name, "openbb-build"])
.output()
.map_err(|e| format!("Failed to run openbb-build: {e}"))?;
if !build_output.status.success() {
let stderr = String::from_utf8_lossy(&build_output.stderr);
log::debug!("Warning: openbb-build failed, continuing anyway: {stderr}");
}
Ok(())
/// Create default backend services (OpenBB API and MCP)
/// This should only be called after a successful full installation
#[tauri::command]
pub async fn create_default_backend_services() -> Result<(), String> {
use crate::tauri_handlers::helpers::{RealEnvSystem, RealFileExtTrait, RealFileSystem};
create_default_backend_services_impl(&RealFileSystem, &RealEnvSystem, &RealFileExtTrait).await
}
async fn create_default_backend_service<F: FileSystem, E: EnvSystem, FE: FileExtTrait>(
async fn create_default_backend_services_impl<F: FileSystem, E: EnvSystem, FE: FileExtTrait>(
fs: &F,
env_sys: &E,
file_ext: &FE,
) {
) -> Result<(), String> {
let backend = crate::tauri_handlers::backends::BackendService {
id: uuid::Uuid::new_v4().to_string(),
name: "OpenBB API".to_string(),
@@ -1535,6 +1476,8 @@ async fn create_default_backend_service<F: FileSystem, E: EnvSystem, FE: FileExt
url: None,
};
let _ = create_backend_service_impl(mcp_backend, fs, env_sys, file_ext);
Ok(())
}
async fn generate_environment_yaml<F: FileSystem, E: EnvSystem>(
@@ -1932,7 +1875,7 @@ mod tests {
fs.expect_create_dir_all().returning(|_| Ok(()));
create_default_backend_service(&fs, &env_sys, &file_ext).await;
let _ = create_default_backend_services_impl(&fs, &env_sys, &file_ext).await;
// Clean up the temp file
let _ = std::fs::remove_file(&temp_path);

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Open Data Platform by OpenBB",
"version": "1.0.0",
"version": "1.0.1",
"identifier": "co.openbb.platform",
"build": {
"frontendDist": "../dist",
@@ -51,7 +51,7 @@
],
"category": "DeveloperTool",
"publisher": "OpenBB, Inc.",
"copyright": "Copyright © 2025 OpenBB, Inc.",
"copyright": "Copyright © 2026 OpenBB, Inc.",
"license": "AGPLv3",
"licenseFile": "./LICENSE"
},

View File

@@ -156,6 +156,13 @@ export const ExtensionSelector = ({
category: "other-openbb",
credentials: [],
},
{
id: "openbb-cookiecutter",
name: "OpenBB Cookiecutter",
description: "Template for creating new OpenBB extension projects.",
category: "other-openbb",
credentials: [],
},
];
// Update the getFilteredExtensions function to use the new hasMatchingExtensions

10
desktop/src/main.tsx vendored
View File

@@ -3,6 +3,16 @@ import './styles.css';
import { RouterProvider, createRouter } from '@tanstack/react-router';
import { StrictMode } from 'react';
// Suppress known forwardRef warning from Radix UI in @openbb/ui-pro
// This is a harmless warning from older Radix UI versions
const originalError = console.error;
console.error = (...args) => {
if (typeof args[0] === 'string' && args[0].includes('forwardRef render functions accept exactly two parameters')) {
return;
}
originalError.apply(console, args);
};
// Import the generated route tree
import { routeTree } from './routeTree.gen'

View File

@@ -156,6 +156,13 @@ const ExtensionSelector = ({
category: "other-openbb",
credentials: [],
},
{
id: "openbb-cookiecutter",
name: "OpenBB Cookiecutter",
description: "Template for creating new OpenBB extension projects.",
category: "other-openbb",
credentials: [],
}
];
// Add a custom package
@@ -1185,7 +1192,7 @@ export default function InstallationProgress() {
setIsComplete(true);
};
// Handle completion - continue to app
// Handle completion - continue to app (only for successful installations)
const handleContinue = async () => {
setIsContinuing(true);
// Instead of using navigate, use window.location to force a full page reload
@@ -1198,13 +1205,39 @@ export default function InstallationProgress() {
try {
await invoke("update_openbb_settings", {
condaDir: directory,
environment: "openbb",
});
condaDir: directory,
environment: "openbb",
});
} catch (error) {
console.error("Failed to update OpenBB settings:", error);
// Proceed to app even if this fails
}
// Create default backend services only on successful installation
try {
await invoke("create_default_backend_services");
} catch (error) {
console.error("Failed to create default backend services:", error);
// Proceed to app even if this fails
}
window.localStorage.setItem("environments-first-load-done", "true");
window.location.href = `/environments${queryString ? `?${queryString}` : ""}`;
};
// Handle "Continue Anyway" when installation has failed
// This skips settings updates since the environment may be incomplete
const handleContinueAnyway = () => {
setIsContinuing(true);
const searchParams = new URLSearchParams();
if (directory) searchParams.append("directory", directory);
if (userDataDir) searchParams.append("userDataDir", userDataDir);
const queryString = searchParams.toString();
// Don't update settings or create backend configs for failed installations
// Just navigate to environments so user can see what's available
console.warn("Continuing after failed installation - settings not updated");
window.localStorage.setItem("environments-first-load-done", "true");
window.location.href = `/environments${queryString ? `?${queryString}` : ""}`;
};
@@ -1458,7 +1491,34 @@ export default function InstallationProgress() {
<div className="mt-4 mb-4 p-4 bg-red-900/30 text-red-300 rounded-md">
<p className="body-md-bold">Installation failed</p>
<p className="mt-2 body-sm-regular overflow-auto max-h-40">{error}</p>
<p className="mt-3 body-xs-regular text-theme-secondary">
For common installation issues (permissions, missing compilers, etc.), see the{" "}
<a
href="https://docs.openbb.co/odp/desktop/troubleshooting"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 underline"
>
troubleshooting guide
</a>.
</p>
<div className="mt-4 p-3 bg-yellow-900/20 border border-yellow-600/30 rounded-md">
<p className="body-xs-bold text-yellow-300">What happens if you continue?</p>
<ul className="mt-2 body-xs-regular text-yellow-200/80 list-disc list-inside space-y-1">
<li>The environment may be incomplete or non-functional</li>
<li>Default backend services (OpenBB API, MCP) will not be configured</li>
<li>You may need to manually set up the environment later</li>
</ul>
</div>
<div className="mt-4 flex gap-2 justify-end">
<Button
variant="secondary"
onClick={handleContinueAnyway}
className="button-secondary px-2 py-1"
size="sm"
>
Continue Anyway
</Button>
<Button
variant="destructive"
onClick={handleTryAgain}
@@ -1467,14 +1527,6 @@ export default function InstallationProgress() {
>
Try Again
</Button>
<Button
variant="secondary"
onClick={handleContinue}
className="button-secondary px-2 py-1"
size="sm"
>
Continue Anyway
</Button>
</div>
</div>
)}

View File

@@ -303,6 +303,9 @@ describe('BackendsPage', () => {
});
test('handles delete error gracefully', async () => {
// Suppress expected console.error output for this error handling test
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const mockBackend = {
id: 'test-backend',
name: 'Test Backend',
@@ -331,6 +334,10 @@ describe('BackendsPage', () => {
await act(async () => { fireEvent.click(within(modal as HTMLElement).getByRole('button', { name: /Delete/i })); });
await waitFor(() => expect(screen.getByText(/Failed to delete backend/i)).toBeInTheDocument());
// Verify console.error was called and restore it
expect(consoleErrorSpy).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
test('views backend logs', async () => {

View File

@@ -1,4 +1,13 @@
import '@testing-library/jest-dom';
import React from 'react';
// Mock @openbb/ui-pro to avoid forwardRef warnings in tests
vi.mock('@openbb/ui-pro', () => ({
Tooltip: ({ children, content }: { children: React.ReactNode; content: string }) =>
React.createElement('div', { 'data-tooltip-content': content }, children),
Button: ({ children, ...props }: { children: React.ReactNode; [key: string]: unknown }) =>
React.createElement('button', props, children),
}));
// Mock ResizeObserver
const ResizeObserverMock = vi.fn(() => ({