From b8fe9fc95ae5f9fcce06beae361175b1d6aae03e Mon Sep 17 00:00:00 2001 From: giswqs Date: Sat, 14 Nov 2026 00:42:01 -0500 Subject: [PATCH 1/2] feat(mobile): a phone's build runs no other programs, and says so where GDAL, DuckDB and GeoLibre would appear (#1902) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rustgis_data::RUNS_PROGRAMS is false on Android and iOS (as in the browser): no program is looked for, and a format read through GDAL, a DuckDB database, the SQL pane, Add Data's Database… row and the GeoLibre tools say a phone can't run their program instead of asking for an install. --- apps/rustgis-mobile/README.md | 2 +- crates/data/src/lib.rs | 18 ++++++++++++++++++ crates/data/src/raster/gdal.rs | 23 ++++++++++++++++++++--- crates/data/src/source/duckdb.rs | 23 ++++++++++++++++++++--- crates/data/src/source/external.rs | 6 +++++- crates/data/src/source/gdal.rs | 23 ++++++++++++++++++++--- crates/engine/src/cmd/layer.rs | 9 ++++++++- crates/ui-egui/src/chrome.rs | 4 ++++ crates/ui-egui/src/panes.rs | 24 ++++++++++++++++++++++++ crates/ui-egui/src/ribbon.rs | 10 ++++++++-- crates/ui-egui/src/sqlpane.rs | 8 ++++++++ 11 files changed, 136 insertions(+), 14 deletions(-) diff --git a/apps/rustgis-mobile/README.md b/apps/rustgis-mobile/README.md index 2252f4d5d6..cd10d3dd62 100644 --- a/apps/rustgis-mobile/README.md +++ b/apps/rustgis-mobile/README.md @@ -115,7 +115,7 @@ one's. | File dialogs | Android's picker, through `Services::open_async` and `download` as in the browser. `rfd` has no Android backend, so what still asks it (choosing a folder) is cancelled and says so (`crates/ui-egui/src/filedialog.rs`) | | HTTPS trust | The bundled Mozilla roots, not the device's certificates; the session says so once (`rustgis_data::http`) | | Folders | Settings, app data and the cache are in the app's internal storage, set once at start-up (`rustgis_data::dirs::set_root`) | -| GeoLibre tools, GDAL formats, DuckDB | Not available: a phone can't run their programs | +| GeoLibre tools, GDAL formats, DuckDB | Not available: a phone can't run their programs. `rustgis_data::RUNS_PROGRAMS` is false on Android and iOS, so no program is looked for, and each place they would appear says so: a file read through GDAL or a DuckDB database is refused with the reason (convert it on a computer), the SQL pane says why it can't query, Add Data's Database… row and its Command Search row are unavailable with the reason, and the Geoprocessing pane shows a card in place of the GeoLibre tools. The app is built without the engine's `geolibre` feature | | Preferences, the keychain, autosave | Not wired up yet | | Soft keyboard | It opens for a text field. `NativeActivity`'s is poor; a real app moves to `GameActivity` | diff --git a/crates/data/src/lib.rs b/crates/data/src/lib.rs index ecc593c471..b93eac3ea1 100644 --- a/crates/data/src/lib.rs +++ b/crates/data/src/lib.rs @@ -61,6 +61,24 @@ pub use value::{Field, FieldType, Value}; use serde::{Deserialize, Serialize}; +/// Whether this build runs other programs: GDAL's (for the formats read through it), DuckDB (SQL +/// queries and DuckDB databases) and the `geolibre` runner. Not in the browser, which runs none, +/// nor on a phone (Android, iOS), whose apps can't start them (#1902): there, what would run one +/// says so (see [`no_programs_here`]) instead of asking for an install that can't be made. +pub const RUNS_PROGRAMS: bool = !cfg!(any(target_arch = "wasm32", target_os = "android", target_os = "ios")); + +/// What runs RustGIS where [`RUNS_PROGRAMS`] is false, for a message that says a program can't run +/// there: "a phone" or "the browser". Empty where programs run. +pub fn no_programs_here() -> &'static str { + if RUNS_PROGRAMS { + "" + } else if cfg!(target_arch = "wasm32") { + "the browser" + } else { + "a phone" + } +} + #[derive(Debug, thiserror::Error)] pub enum DataError { #[error("i/o: {0}")] diff --git a/crates/data/src/raster/gdal.rs b/crates/data/src/raster/gdal.rs index 75c9acc776..5063f09db6 100644 --- a/crates/data/src/raster/gdal.rs +++ b/crates/data/src/raster/gdal.rs @@ -126,10 +126,20 @@ pub fn convert_jpeg2000(bytes: Vec, crs: Option<&str>, max: u64) -> Result DataError { - DataError::Unsupported( + DataError::Unsupported(missing_text(crate::no_programs_here())) +} + +/// Why a raster format read through GDAL can't be read: GDAL isn't installed, or (`here` names +/// it) this build runs no programs, where installing it wouldn't help. +fn missing_text(here: &str) -> String { + if here.is_empty() { "this raster format needs GDAL: install it (gdal_translate and gdalinfo on the PATH, or RUSTGIS_GDAL_BIN naming their folder) and add the file again" - .into(), - ) + .into() + } else { + format!( + "this raster format is read through GDAL, a program {here} can't run: convert the file to a GeoTIFF on a computer and add that" + ) + } } /// Refuses an ER Mapper header that would open more than its own data: any but an `ERStorage` one @@ -496,6 +506,13 @@ mod tests { use super::*; use std::process::Command; + #[test] + fn where_programs_cant_run_the_message_says_so_instead_of_asking_for_an_install() { + assert!(missing_text("").contains("install it")); + let phone = missing_text("a phone"); + assert!(phone.contains("a phone can't run") && !phone.contains("install"), "{phone}"); + } + #[test] fn names() { assert!(is_gdal_raster_name("/d/N45E006.HGT")); diff --git a/crates/data/src/source/duckdb.rs b/crates/data/src/source/duckdb.rs index 4639a1e745..ad6a408eb3 100644 --- a/crates/data/src/source/duckdb.rs +++ b/crates/data/src/source/duckdb.rs @@ -281,10 +281,20 @@ fn live_not_here() -> DataError { } fn missing() -> DataError { - DataError::Unsupported( + DataError::Unsupported(missing_text(crate::no_programs_here())) +} + +/// Why a query or a DuckDB database can't be read: DuckDB isn't installed, or (`here` names it) +/// this build runs no programs, where installing it wouldn't help. +fn missing_text(here: &str) -> String { + if here.is_empty() { "SQL queries and DuckDB databases need DuckDB: install it (the duckdb program on the PATH, or RUSTGIS_DUCKDB_BIN naming its folder) and try again" - .into(), - ) + .into() + } else { + format!( + "SQL queries and DuckDB databases run through DuckDB, a program {here} can't run: open this project on a computer to use them" + ) + } } /// Where results are kept: the platform's cache folder, or `RUSTGIS_DUCKDB_CACHE`. @@ -966,6 +976,13 @@ fn ours(name: &str) -> bool { mod tests { use super::*; + #[test] + fn where_programs_cant_run_the_message_says_so_instead_of_asking_for_an_install() { + assert!(missing_text("").contains("install it")); + let phone = missing_text("a phone"); + assert!(phone.contains("a phone can't run") && !phone.contains("install"), "{phone}"); + } + /// Each kind reads only its own client's variable, and an empty one is none (a fake /// environment: the process's is never changed). #[test] diff --git a/crates/data/src/source/external.rs b/crates/data/src/source/external.rs index 9bfe55558c..65441b6e9d 100644 --- a/crates/data/src/source/external.rs +++ b/crates/data/src/source/external.rs @@ -5,8 +5,12 @@ use crate::Result; use std::path::{Path, PathBuf}; use std::process::Command; -/// A program: in the folder `env` names, else on the `PATH`. +/// A program: in the folder `env` names, else on the `PATH`. Never one in a build that can't run +/// programs ([`crate::RUNS_PROGRAMS`]): a phone may have a file of that name, but can't start it. pub(crate) fn program(env: &str, name: &str) -> Option { + if !crate::RUNS_PROGRAMS { + return None; + } let exe = if cfg!(windows) { format!("{name}.exe") } else { name.to_string() }; let dirs: Vec = std::env::var_os(env) .map(PathBuf::from) diff --git a/crates/data/src/source/gdal.rs b/crates/data/src/source/gdal.rs index d7c3b5bc62..76b3206306 100644 --- a/crates/data/src/source/gdal.rs +++ b/crates/data/src/source/gdal.rs @@ -107,10 +107,18 @@ pub fn usable() -> bool { } fn missing() -> DataError { - DataError::Unsupported( + DataError::Unsupported(missing_text(crate::no_programs_here())) +} + +/// Why a format read through GDAL can't be read: GDAL isn't installed, or (`here` names it) this +/// build runs no programs, where installing it wouldn't help. +fn missing_text(here: &str) -> String { + if here.is_empty() { "this format needs GDAL: install it (ogr2ogr and ogrinfo on the PATH, or RUSTGIS_GDAL_BIN naming their folder) and add the file again" - .into(), - ) + .into() + } else { + format!("this format is read through GDAL, a program {here} can't run: convert the file to a GeoPackage on a computer and add that") + } } /// Where converted layers are kept: the platform's cache folder, or `RUSTGIS_GDAL_CACHE`. @@ -606,6 +614,15 @@ mod tests { use super::*; use crate::GeometryType; + #[test] + fn where_programs_cant_run_the_message_says_so_instead_of_asking_for_an_install() { + assert!(missing_text("").contains("install it")); + let phone = missing_text("a phone"); + assert!(phone.contains("a phone can't run") && !phone.contains("install"), "{phone}"); + // Where programs run, the build reports none missing for that reason. + assert_eq!(crate::RUNS_PROGRAMS, crate::no_programs_here().is_empty()); + } + #[test] fn names() { assert!( diff --git a/crates/engine/src/cmd/layer.rs b/crates/engine/src/cmd/layer.rs index 940bcac1e4..21f2fb9a28 100644 --- a/crates/engine/src/cmd/layer.rs +++ b/crates/engine/src/cmd/layer.rs @@ -1587,7 +1587,14 @@ fn add_geodatabase(s: &mut Session, p: &Value, path: &str) -> CmdResult { match gdal.then(|| add_gdal(s, &q, path)) { Some(Ok(r)) => results.push(r), Some(Err(g)) => s.notify(format!("{path}: {table} not read: {why}; through GDAL: {g}"), true), - None => s.notify(format!("{path}: {table} not read: {why} (GDAL, when installed, reads it)"), true), + None if rustgis_data::RUNS_PROGRAMS => { + s.notify(format!("{path}: {table} not read: {why} (GDAL, when installed, reads it)"), true) + } + // Installing GDAL is no help where no program runs (a phone, #1902). + None => s.notify( + format!("{path}: {table} not read: {why} (GDAL reads it, a program {} can't run)", rustgis_data::no_programs_here()), + true, + ), } } if results.is_empty() { diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index f154918b87..a6adec8c2e 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -1035,6 +1035,10 @@ fn search_action(id: &str) -> Option { /// Desktop-only interactive actions stay visible but disabled in web Command Search. fn search_platform_disabled(id: &str, web: bool) -> Option<&'static str> { + // A phone runs the desktop's code but, like the browser, no other program (#1902). + if !web && !rustgis_engine::data::RUNS_PROGRAMS && id == "layer.addDatabase" { + return Some("databases are read through DuckDB, a program a phone can't run"); + } if !web { return None; } diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs index fd2c4f3228..b18c1c2202 100644 --- a/crates/ui-egui/src/panes.rs +++ b/crates/ui-egui/src/panes.rs @@ -3155,12 +3155,36 @@ impl GpForm { } } +/// The card that stands for the GeoLibre tools on a phone, which can't run their program. +fn no_geolibre_here(ui: &mut Ui, t: &Tokens) { + ui.add_space(6.0); + egui::Frame::NONE.stroke(Stroke::new(1.0, t.border)).corner_radius(CornerRadius::same(6)).inner_margin(egui::Margin::same(10)).show( + ui, + |ui| { + ui.set_width(ui.available_width()); + ui.label(egui::RichText::new(tl!("Whitebox and GeoLibre tools")).font(semibold(13.0))); + ui.label( + egui::RichText::new(tl!( + "Not on a phone: these tools run as a program of their own, which a phone can't run. Use them in RustGIS on a computer." + )) + .size(11.5) + .color(t.text_dim), + ); + }, + ); +} + /// Where to get the GeoLibre tools when the runner isn't installed (desktop and CLI builds only). fn geolibre_card(app: &mut RustGisApp, ui: &mut Ui, t: &Tokens) { // Read directly, not through Session::run: a command each frame would bump the session's // revision and make the views redraw. The status itself is cached in the runner module. let status = rustgis_engine::cmd::gp::geolibre_status(); if status["supported"] != true { + // A phone's build leaves the suite out (its runner is a program, which a phone's apps + // can't start): said where the card would be, so the tools aren't simply missing (#1902). + if !rustgis_engine::data::RUNS_PROGRAMS && !cfg!(target_arch = "wasm32") { + no_geolibre_here(ui, t); + } return; } // An install that just finished: use the runner, and remember it. diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index bfaa80c292..8ffe2984ec 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -506,9 +506,15 @@ fn home_tab(app: &mut RustGisApp, ui: &mut Ui) { menu_item(ui, app, "", "From URL…", "ui.addUrl", Value::Null); menu_item(ui, app, "", "From Service…", "ui.addService", Value::Null); menu_item(ui, app, "", "Zarr…", "ui.dialog", json!({"name": "zarr"})); - // Databases are read through DuckDB, a program the browser can't run. + // Databases are read through DuckDB, a program the browser can't run. A phone + // can't either: there the row stays, unavailable, and says why (#1902). #[cfg(not(target_arch = "wasm32"))] - menu_item(ui, app, "", "Database…", "ui.dialog", json!({"name": "addDatabase"})); + if rustgis_engine::data::RUNS_PROGRAMS { + menu_item(ui, app, "", "Database…", "ui.dialog", json!({"name": "addDatabase"})); + } else { + crate::widgets::menu_row(ui, "", tl!("Database…"), "", false) + .on_disabled_hover_text(tl!("Databases are read through DuckDB, a program a phone can't run.")); + } #[cfg(not(target_arch = "wasm32"))] menu_item(ui, app, "", "Edit GeoPackage Table…", "ui.dialog", json!({"name": "gpkgAddLayer"})); menu_item(ui, app, "", "S3…", "ui.dialog", json!({"name": "s3"})); diff --git a/crates/ui-egui/src/sqlpane.rs b/crates/ui-egui/src/sqlpane.rs index 6fc14be603..fc3302728d 100644 --- a/crates/ui-egui/src/sqlpane.rs +++ b/crates/ui-egui/src/sqlpane.rs @@ -313,6 +313,14 @@ fn body(app: &mut RustGisApp, ui: &mut Ui, t: &Tokens) { ); return; } + // A phone's apps can't start another program (#1902). + if !rustgis_engine::data::RUNS_PROGRAMS { + ui.label( + RichText::new(tl!("SQL queries run through DuckDB, a program a phone can't run: open this project on a computer to query it.")) + .weak(), + ); + return; + } if !app.ui.sql.history_loaded { app.ui.sql.history_loaded = true; app.ui.sql.history = app From f259cc3ab641e2044eabdeb4ac0bda62e3391c61 Mon Sep 17 00:00:00 2001 From: giswqs Date: Sat, 14 Nov 2026 03:08:31 -0500 Subject: [PATCH 2/2] =?UTF-8?q?fix(mobile):=20keep=20Database=E2=80=A6=20o?= =?UTF-8?q?n=20a=20phone,=20whose=20live=20connections=20need=20no=20progr?= =?UTF-8?q?am?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/rustgis-mobile/README.md | 2 +- crates/ui-egui/src/chrome.rs | 4 ---- crates/ui-egui/src/ribbon.rs | 12 ++++-------- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/apps/rustgis-mobile/README.md b/apps/rustgis-mobile/README.md index cd10d3dd62..e9d0f0dbc6 100644 --- a/apps/rustgis-mobile/README.md +++ b/apps/rustgis-mobile/README.md @@ -115,7 +115,7 @@ one's. | File dialogs | Android's picker, through `Services::open_async` and `download` as in the browser. `rfd` has no Android backend, so what still asks it (choosing a folder) is cancelled and says so (`crates/ui-egui/src/filedialog.rs`) | | HTTPS trust | The bundled Mozilla roots, not the device's certificates; the session says so once (`rustgis_data::http`) | | Folders | Settings, app data and the cache are in the app's internal storage, set once at start-up (`rustgis_data::dirs::set_root`) | -| GeoLibre tools, GDAL formats, DuckDB | Not available: a phone can't run their programs. `rustgis_data::RUNS_PROGRAMS` is false on Android and iOS, so no program is looked for, and each place they would appear says so: a file read through GDAL or a DuckDB database is refused with the reason (convert it on a computer), the SQL pane says why it can't query, Add Data's Database… row and its Command Search row are unavailable with the reason, and the Geoprocessing pane shows a card in place of the GeoLibre tools. The app is built without the engine's `geolibre` feature | +| GeoLibre tools, GDAL formats, DuckDB | Not available: a phone can't run their programs. `rustgis_data::RUNS_PROGRAMS` is false on Android and iOS, so no program is looked for, and each place they would appear says so: a file read through GDAL or a DuckDB database is refused with the reason (convert it on a computer), the SQL pane says why it can't query, and the Geoprocessing pane shows a card in place of the GeoLibre tools. Add Data's Database… stays: its PostGIS, MySQL and SQL Server connections need no program. The app is built without the engine's `geolibre` feature | | Preferences, the keychain, autosave | Not wired up yet | | Soft keyboard | It opens for a text field. `NativeActivity`'s is poor; a real app moves to `GameActivity` | diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index a6adec8c2e..f154918b87 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -1035,10 +1035,6 @@ fn search_action(id: &str) -> Option { /// Desktop-only interactive actions stay visible but disabled in web Command Search. fn search_platform_disabled(id: &str, web: bool) -> Option<&'static str> { - // A phone runs the desktop's code but, like the browser, no other program (#1902). - if !web && !rustgis_engine::data::RUNS_PROGRAMS && id == "layer.addDatabase" { - return Some("databases are read through DuckDB, a program a phone can't run"); - } if !web { return None; } diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 8ffe2984ec..0bc58b1ced 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -506,15 +506,11 @@ fn home_tab(app: &mut RustGisApp, ui: &mut Ui) { menu_item(ui, app, "", "From URL…", "ui.addUrl", Value::Null); menu_item(ui, app, "", "From Service…", "ui.addService", Value::Null); menu_item(ui, app, "", "Zarr…", "ui.dialog", json!({"name": "zarr"})); - // Databases are read through DuckDB, a program the browser can't run. A phone - // can't either: there the row stays, unavailable, and says why (#1902). + // Databases are read through DuckDB, a program the browser can't run. A phone keeps + // the row: its PostGIS, MySQL and SQL Server connections need no program, and a + // DuckDB database refused there says why (#1902). #[cfg(not(target_arch = "wasm32"))] - if rustgis_engine::data::RUNS_PROGRAMS { - menu_item(ui, app, "", "Database…", "ui.dialog", json!({"name": "addDatabase"})); - } else { - crate::widgets::menu_row(ui, "", tl!("Database…"), "", false) - .on_disabled_hover_text(tl!("Databases are read through DuckDB, a program a phone can't run.")); - } + menu_item(ui, app, "", "Database…", "ui.dialog", json!({"name": "addDatabase"})); #[cfg(not(target_arch = "wasm32"))] menu_item(ui, app, "", "Edit GeoPackage Table…", "ui.dialog", json!({"name": "gpkgAddLayer"})); menu_item(ui, app, "", "S3…", "ui.dialog", json!({"name": "s3"}));