You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by pa...@apache.org on 2026/10/20 01:11:24 UTC
(sedona-db) branch main updated: feat(r/sedonadb): configure GDAL and PROJ at runtime (#1449)
This is an automated email from the ASF dual-hosted git repository.
paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git
The following commit(s) were added to refs/heads/main by this push:
new cce7b9112 feat(r/sedonadb): configure GDAL and PROJ at runtime (#1449)
cce7b9112 is described below
commit cce7b91124450eeb879c224b1fc2d707438f5ce9
Author: Dewey Dunnington <de...@dunnington.ca>
AuthorDate: Mon Oct 19 20:11:18 2026 -0500
feat(r/sedonadb): configure GDAL and PROJ at runtime (#1449)
---
Cargo.lock | 1 +
rust/sedona/src/lib.rs => c/sedona-gdal/build.rs | 30 ++-
c/sedona-gdal/src/dyn_load.rs | 3 +-
c/sedona-gdal/src/gdal_api.rs | 191 ++++++++++++++++++-
c/sedona-gdal/src/global.rs | 36 ++--
python/sedonadb/python/sedonadb/context.py | 114 ++---------
python/sedonadb/src/lib.rs | 61 ++++++
r/sedonadb/DESCRIPTION | 3 +-
r/sedonadb/NAMESPACE | 1 +
r/sedonadb/R/000-wrappers.R | 25 +++
r/sedonadb/R/context.R | 83 +++++---
r/sedonadb/R/zzz.R | 18 ++
r/sedonadb/README.Rmd | 4 +-
r/sedonadb/README.md | 19 +-
r/sedonadb/cleanup.win | 1 +
r/sedonadb/configure.win | 199 +++++++++++++------
r/sedonadb/man/sd_configure_gdal.Rd | 21 +++
r/sedonadb/man/sd_configure_proj.Rd | 1 +
r/sedonadb/src/Makevars.win.in | 27 ++-
r/sedonadb/src/init.c | 31 +++
r/sedonadb/{cleanup.win => src/install.libs.R} | 12 +-
r/sedonadb/src/rust/Cargo.toml | 4 +
r/sedonadb/src/rust/api.h | 5 +
r/sedonadb/src/rust/src/lib.rs | 64 +++++++
r/sedonadb/tests/testthat/test-context.R | 12 ++
.../sedonadb-arm64-mingw-spatial-release.cmake} | 5 +-
.../sedonadb-mingw-static-release.cmake | 4 +-
.../sedonadb-x64-mingw-spatial-release.cmake} | 5 +-
rust/sedona/src/lib.rs | 1 +
rust/sedona/src/native_library.rs | 210 +++++++++++++++++++++
30 files changed, 955 insertions(+), 236 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index b88d1fb19..4231e8cb5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6572,6 +6572,7 @@ dependencies = [
"sedona-adbc",
"sedona-expr",
"sedona-extension",
+ "sedona-gdal",
"sedona-geometry",
"sedona-geoparquet",
"sedona-proj",
diff --git a/rust/sedona/src/lib.rs b/c/sedona-gdal/build.rs
similarity index 68%
copy from rust/sedona/src/lib.rs
copy to c/sedona-gdal/build.rs
index 907848843..59442ec40 100644
--- a/rust/sedona/src/lib.rs
+++ b/c/sedona-gdal/build.rs
@@ -14,19 +14,17 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-pub mod catalog;
-mod catalog_planner;
-pub mod context;
-pub mod context_builder;
-mod exec;
-pub mod memory_pool;
-mod object_storage;
-pub use object_storage::is_object_store_option;
-pub mod pool_type;
-pub mod projected_reader;
-pub mod random_geometry_provider;
-mod read;
-pub mod record_batch_reader_provider;
-pub mod show;
-pub mod size_parser;
-pub mod url_table;
+
+const GDAL_3_13_0: u32 = 3_130_000;
+
+fn main() {
+ println!("cargo:rustc-check-cfg=cfg(gdal_has_mem_create)");
+
+ if std::env::var("DEP_GDAL_VERSION_NUMBER")
+ .ok()
+ .and_then(|value| value.parse::<u32>().ok())
+ .is_some_and(|version| version >= GDAL_3_13_0)
+ {
+ println!("cargo:rustc-cfg=gdal_has_mem_create");
+ }
+}
diff --git a/c/sedona-gdal/src/dyn_load.rs b/c/sedona-gdal/src/dyn_load.rs
index 1d1cacfa1..9b5e90c90 100644
--- a/c/sedona-gdal/src/dyn_load.rs
+++ b/c/sedona-gdal/src/dyn_load.rs
@@ -264,8 +264,7 @@ fn current_process_library() -> Result<Library, GdalInitLibraryError> {
#[cfg(windows)]
fn current_process_library() -> Result<Library, GdalInitLibraryError> {
- // Safety: loading symbols from the current process is safe.
- Ok(unsafe { libloading::os::windows::Library::this() }
+ Ok(libloading::os::windows::Library::this()
.map_err(|e| {
GdalInitLibraryError::LibraryError(format!(
"Failed to open current process handle: {}",
diff --git a/c/sedona-gdal/src/gdal_api.rs b/c/sedona-gdal/src/gdal_api.rs
index 84f1e8695..edf8be3fa 100644
--- a/c/sedona-gdal/src/gdal_api.rs
+++ b/c/sedona-gdal/src/gdal_api.rs
@@ -24,6 +24,63 @@ use crate::dyn_load;
use crate::errors::{GdalError, GdalInitLibraryError};
use crate::gdal_dyn_bindgen::{GDALTransformerFunc, SedonaGdalApi};
+#[cfg(feature = "gdal-sys")]
+unsafe extern "C" {
+ #[link_name = "VRTCreate"]
+ fn gdal_sys_vrt_create(x_size: i32, y_size: i32) -> *mut std::ffi::c_void;
+ #[link_name = "VRTAddSimpleSource"]
+ fn gdal_sys_vrt_add_simple_source(
+ vrt_band: *mut std::ffi::c_void,
+ source_band: *mut std::ffi::c_void,
+ src_x_off: i32,
+ src_y_off: i32,
+ src_x_size: i32,
+ src_y_size: i32,
+ dst_x_off: i32,
+ dst_y_off: i32,
+ dst_x_size: i32,
+ dst_y_size: i32,
+ resampling: *const std::ffi::c_char,
+ nodata: f64,
+ ) -> i32;
+}
+
+// gdal-sys 0.12 does not expose MEMCreate, even when its bindgen feature is
+// enabled: gdal_mem.h is not included in its wrapper and MEM functions are not
+// in its allowlist. Declare the stable C ABI locally when the linked GDAL has
+// the symbol.
+#[cfg(all(feature = "gdal-sys", gdal_has_mem_create))]
+unsafe extern "C" {
+ #[link_name = "MEMCreate"]
+ fn gdal_sys_mem_create(
+ x_size: i32,
+ y_size: i32,
+ bands: i32,
+ data_type: crate::gdal_dyn_bindgen::GDALDataType,
+ options: *const *const std::ffi::c_char,
+ ) -> *mut std::ffi::c_void;
+}
+
+// GDAL 3.12 and earlier predate the public MEMCreate C API. Unix and MinGW use
+// the Itanium C++ ABI, so referencing the private constructor preserves the
+// fast path and makes static linkers retain its object from libgdal.a.
+#[cfg(all(
+ feature = "gdal-sys",
+ not(gdal_has_mem_create),
+ any(unix, all(windows, target_env = "gnu"))
+))]
+unsafe extern "C" {
+ #[link_name = "_ZN10MEMDataset6CreateEPKciii12GDALDataTypePPc"]
+ fn gdal_sys_mem_dataset_create(
+ filename: *const std::ffi::c_char,
+ x_size: i32,
+ y_size: i32,
+ bands: i32,
+ data_type: crate::gdal_dyn_bindgen::GDALDataType,
+ options: *mut *mut std::ffi::c_char,
+ ) -> *mut std::ffi::c_void;
+}
+
/// Invoke a function pointer from the `SedonaGdalApi` struct.
///
/// # Panics
@@ -48,10 +105,10 @@ pub(crate) use call_gdal_api;
#[derive(Debug)]
pub struct GdalApi {
pub(crate) inner: SedonaGdalApi,
- /// The dynamically loaded GDAL library. Kept alive for the lifetime of the
- /// function pointers in `inner`. This is never dropped because the `GdalApi`
- /// lives in a `static OnceLock` (see `global.rs`).
- _lib: Library,
+ /// The dynamically loaded GDAL library, when applicable. Kept alive for the
+ /// lifetime of the function pointers in `inner`. Statically linked API
+ /// tables do not need a library handle.
+ _lib: Option<Library>,
name: String,
}
@@ -60,7 +117,7 @@ impl GdalApi {
let (lib, inner) = dyn_load::load_gdal_from_path(&shared_library)?;
Ok(Self {
inner,
- _lib: lib,
+ _lib: Some(lib),
name: shared_library.to_string_lossy().into_owned(),
})
}
@@ -69,11 +126,133 @@ impl GdalApi {
let (lib, inner) = dyn_load::load_gdal_from_current_process()?;
Ok(Self {
inner,
- _lib: lib,
+ _lib: Some(lib),
name: "current_process".to_string(),
})
}
+ /// Build the API table directly from functions linked by `gdal-sys`.
+ ///
+ /// Linked GDAL functions are not guaranteed to be exported from the final
+ /// executable or DLL for runtime symbol lookup, so retain their addresses
+ /// directly when the platform has a supported MEM creator ABI.
+ #[cfg(all(feature = "gdal-sys", any(unix, all(windows, target_env = "gnu"))))]
+ pub fn from_gdal_sys() -> Self {
+ let mut inner = SedonaGdalApi::default();
+
+ macro_rules! set_fn {
+ ($name:ident) => {
+ // The generated gdal-sys bindings model C enums as integers,
+ // while Sedona's dynamic table uses repr(C) enums. Their ABIs
+ // are identical, but their Rust function-pointer types differ.
+ #[allow(clippy::missing_transmute_annotations)]
+ {
+ inner.$name =
+ Some(unsafe { std::mem::transmute(gdal_sys::$name as *const ()) });
+ }
+ };
+ }
+
+ set_fn!(GDALOpenEx);
+ set_fn!(GDALClose);
+ set_fn!(GDALGetRasterXSize);
+ set_fn!(GDALGetRasterYSize);
+ set_fn!(GDALGetRasterCount);
+ set_fn!(GDALGetRasterBand);
+ set_fn!(GDALGetGeoTransform);
+ set_fn!(GDALSetGeoTransform);
+ set_fn!(GDALGetProjectionRef);
+ set_fn!(GDALSetProjection);
+ set_fn!(GDALGetSpatialRef);
+ set_fn!(GDALSetSpatialRef);
+ set_fn!(GDALCreateCopy);
+ set_fn!(GDALDatasetCreateLayer);
+ set_fn!(GDALAllRegister);
+ set_fn!(GDALGetDriverByName);
+ set_fn!(GDALCreate);
+ set_fn!(GDALAddBand);
+ set_fn!(GDALRasterIO);
+ set_fn!(GDALRasterIOEx);
+ set_fn!(GDALGetRasterDataType);
+ set_fn!(GDALGetRasterBandXSize);
+ set_fn!(GDALGetRasterBandYSize);
+ set_fn!(GDALGetBlockSize);
+ set_fn!(GDALGetRasterNoDataValue);
+ set_fn!(GDALGetRasterNoDataValueAsUInt64);
+ set_fn!(GDALGetRasterNoDataValueAsInt64);
+ set_fn!(GDALSetRasterNoDataValue);
+ set_fn!(GDALDeleteRasterNoDataValue);
+ set_fn!(GDALSetRasterNoDataValueAsUInt64);
+ set_fn!(GDALSetRasterNoDataValueAsInt64);
+ set_fn!(OSRNewSpatialReference);
+ set_fn!(OSRSetFromUserInput);
+ set_fn!(OSREPSGTreatsAsLatLong);
+ set_fn!(OSRGetDataAxisToSRSAxisMapping);
+ set_fn!(OSRGetAxisMappingStrategy);
+ set_fn!(OSRSetAxisMappingStrategy);
+ set_fn!(OSRDestroySpatialReference);
+ set_fn!(OSRExportToPROJJSON);
+ set_fn!(OSRExportToWkt);
+ set_fn!(OSRClone);
+ set_fn!(OSRRelease);
+ set_fn!(OGR_G_CreateFromWkb);
+ set_fn!(OGR_G_CreateFromWkt);
+ set_fn!(OGR_G_ExportToIsoWkb);
+ set_fn!(OGR_G_WkbSize);
+ set_fn!(OGR_G_GetEnvelope);
+ set_fn!(OGR_G_DestroyGeometry);
+ set_fn!(OGR_L_ResetReading);
+ set_fn!(OGR_L_GetNextFeature);
+ set_fn!(OGR_L_CreateField);
+ set_fn!(OGR_L_GetFeatureCount);
+ set_fn!(OGR_F_GetGeometryRef);
+ set_fn!(OGR_F_GetFieldIndex);
+ set_fn!(OGR_F_GetFieldAsDouble);
+ set_fn!(OGR_F_GetFieldAsInteger);
+ set_fn!(OGR_F_IsFieldSetAndNotNull);
+ set_fn!(OGR_F_Destroy);
+ set_fn!(OGR_Fld_Create);
+ set_fn!(OGR_Fld_Destroy);
+ set_fn!(VSIFileFromMemBuffer);
+ set_fn!(VSIFCloseL);
+ set_fn!(VSIUnlink);
+ set_fn!(VSIGetMemFileBuffer);
+ set_fn!(VSIFree);
+ set_fn!(VSIMalloc);
+ inner.VRTCreate = Some(gdal_sys_vrt_create);
+ inner.VRTAddSimpleSource = Some(gdal_sys_vrt_add_simple_source);
+ set_fn!(GDALReprojectImage);
+ set_fn!(GDALCreateGenImgProjTransformer);
+ set_fn!(GDALDestroyGenImgProjTransformer);
+ set_fn!(GDALSuggestedWarpOutput);
+ set_fn!(GDALGenImgProjTransform);
+ set_fn!(GDALRasterizeGeometries);
+ set_fn!(GDALFPolygonize);
+ set_fn!(GDALPolygonize);
+ set_fn!(GDALVersionInfo);
+ set_fn!(CPLSetThreadLocalConfigOption);
+ set_fn!(CPLGetLastErrorNo);
+ set_fn!(CPLGetLastErrorMsg);
+ set_fn!(CPLErrorReset);
+ set_fn!(GDALGetDataTypeSizeBytes);
+
+ #[cfg(gdal_has_mem_create)]
+ {
+ inner.MEMCreate = Some(gdal_sys_mem_create);
+ }
+
+ #[cfg(not(gdal_has_mem_create))]
+ {
+ inner.MEMDatasetCreate = Some(gdal_sys_mem_dataset_create);
+ }
+
+ Self {
+ inner,
+ _lib: None,
+ name: "gdal_sys".to_string(),
+ }
+ }
+
pub fn name(&self) -> &str {
&self.name
}
diff --git a/c/sedona-gdal/src/global.rs b/c/sedona-gdal/src/global.rs
index 3de26d7a2..2c212f1f0 100644
--- a/c/sedona-gdal/src/global.rs
+++ b/c/sedona-gdal/src/global.rs
@@ -92,28 +92,27 @@ impl GdalApiBuilder {
/// Build a [`GdalApi`] with the configured options.
///
/// When `shared_library` is set, loads GDAL from that path. Otherwise,
- /// resolves symbols from the current process (with an optional
- /// compile-time version check when the `gdal-sys` feature is enabled).
+ /// uses the linked `gdal-sys` function table when supported, otherwise
+ /// resolves symbols from the current process.
pub fn build(&self) -> Result<GdalApi, GdalInitLibraryError> {
let api = if let Some(shared_library) = &self.shared_library {
GdalApi::try_from_shared_library(shared_library.clone())?
} else {
- GdalApi::try_from_current_process()?
- };
-
- #[cfg(feature = "gdal-sys")]
- let get_gdal_version_info = |arg: &str| unsafe {
- // Calling into `gdal-sys` also forces the linker to include GDAL
- // symbols, so that `try_from_current_process` (which resolves function pointers
- // via `dlsym` on the current process) can find them at runtime.
- let c_arg = std::ffi::CString::new(arg).unwrap();
- let c_version = gdal_sys::GDALVersionInfo(c_arg.as_ptr());
- std::ffi::CStr::from_ptr(c_version)
- .to_string_lossy()
- .into_owned()
+ #[cfg(all(feature = "gdal-sys", any(unix, all(windows, target_env = "gnu"))))]
+ {
+ GdalApi::from_gdal_sys()
+ }
+ #[cfg(not(all(feature = "gdal-sys", any(unix, all(windows, target_env = "gnu")))))]
+ {
+ GdalApi::try_from_current_process().map_err(|e| {
+ GdalInitLibraryError::LibraryError(format!(
+ "GDAL is not configured; call configure_gdal() or \
+ sd_configure_gdal() before using GDAL-backed functions: {e}"
+ ))
+ })?
+ }
};
- #[cfg(not(feature = "gdal-sys"))]
let get_gdal_version_info = |arg: &str| api.version_info(arg);
check_gdal_version(get_gdal_version_info)?;
@@ -272,12 +271,15 @@ mod test {
use super::*;
/// Building with default options (no shared library) should succeed when
- /// `gdal-sys` is linked, loading symbols from the current process.
+ /// `gdal-sys` is linked, using its function table on supported platforms.
#[test]
fn test_builder_default() {
let api = GdalApiBuilder::default()
.build()
.expect("GdalApiBuilder::default().build() should succeed with gdal-sys");
+ #[cfg(any(unix, all(windows, target_env = "gnu")))]
+ assert_eq!(api.name(), "gdal_sys");
+ #[cfg(not(any(unix, all(windows, target_env = "gnu"))))]
assert_eq!(api.name(), "current_process");
}
diff --git a/python/sedonadb/python/sedonadb/context.py b/python/sedonadb/python/sedonadb/context.py
index d87782781..a5c77b485 100644
--- a/python/sedonadb/python/sedonadb/context.py
+++ b/python/sedonadb/python/sedonadb/context.py
@@ -37,7 +37,11 @@ if TYPE_CHECKING:
from sedonadb._lib import (
InternalContext,
configure_gdal_shared,
+ configure_gdal_prefix_native,
+ configure_gdal_system_native,
+ configure_proj_prefix_native,
configure_proj_shared,
+ configure_proj_system_native,
gdal_version as _gdal_version,
)
from sedonadb._options import Options
@@ -815,52 +819,11 @@ def _find_proj_shared_library_near_data(data_dir: Path) -> Optional[Path]:
def _configure_proj_system():
- # For system installs, try common library names that might be on PATH/LD_LIBRARY_PATH
- import ctypes
-
- names_to_try = []
- if sys.platform == "win32":
- names_to_try = ["proj.dll", "proj_9.dll"]
- elif sys.platform == "darwin":
- names_to_try = ["libproj.dylib"]
- else:
- names_to_try = ["libproj.so"]
-
- for name in names_to_try:
- try:
- ctypes.CDLL(name)
- configure_proj(shared_library=name)
- return
- except OSError:
- continue
-
- raise ValueError(
- f"Can't load PROJ shared library '{names_to_try[0]}': "
- "Could not find module (or one of its dependencies). "
- "Try using the full path with constructor syntax."
- )
+ configure_proj_system_native()
def _configure_proj_prefix(prefix: str):
- prefix = Path(prefix)
- if not prefix.exists():
- raise ValueError(f"Can't configure PROJ from prefix '{prefix}': does not exist")
-
- if sys.platform == "win32":
- # Windows conda uses Library/bin for DLLs and Library/share for data
- lib_dir = prefix / "Library" / "bin"
- data_dir = prefix / "Library" / "share" / "proj"
- else:
- lib_dir = prefix / "lib"
- data_dir = prefix / "share" / "proj"
-
- shared_library = _find_proj_shared_library(lib_dir)
-
- configure_proj(
- shared_library=shared_library,
- database_path=data_dir / "proj.db",
- search_path=data_dir,
- )
+ configure_proj_prefix_native(str(prefix))
def configure_gdal(
@@ -917,9 +880,11 @@ def configure_gdal(
return
elif preset == "homebrew":
prefix = os.environ.get("HOMEBREW_PREFIX", "/opt/homebrew")
- shared_library = Path(prefix) / "lib" / _gdal_lib_name()
+ configure_gdal_prefix_native(prefix)
+ return
elif preset == "system":
- shared_library = _gdal_lib_name()
+ configure_gdal_system_native()
+ return
elif preset == "auto":
# The GDAL library bundled with rasterio has more features enabled by default
# (e.g., more compression codecs) than the one bundled with pyogrio, so try
@@ -965,15 +930,6 @@ def configure_gdal(
configure_gdal_shared(str(shared_library))
-def _gdal_lib_name() -> str:
- if sys.platform == "win32":
- return "gdal.dll"
- elif sys.platform == "darwin":
- return "libgdal.dylib"
- else:
- return "libgdal.so"
-
-
def _find_gdal_in_package(pkg_name: str) -> Path:
"""Locate the bundled GDAL shared library inside a pip-installed package.
@@ -1027,42 +983,6 @@ def _configure_gdal_rasterio():
configure_gdal(shared_library=_find_gdal_in_package("rasterio"))
-def _gdal_lib_pattern() -> str:
- """Return a glob pattern for finding GDAL shared libraries."""
- if sys.platform == "win32":
- return "gdal*.dll"
- elif sys.platform == "darwin":
- return "libgdal*.dylib"
- else:
- return "libgdal.so*"
-
-
-def _find_gdal_shared_library(lib_dir: Path) -> Path:
- """Find the GDAL shared library in the given directory.
-
- Raises ValueError if not found or multiple matches.
- """
- pattern = _gdal_lib_pattern()
- possible_files = list(lib_dir.glob(pattern))
-
- # Filter out debug/test variants if multiple matches
- if len(possible_files) > 1:
- # Prefer exact matches or versioned libraries without suffixes like _d
- filtered = [f for f in possible_files if "_d." not in f.name]
- if filtered:
- possible_files = filtered
-
- if len(possible_files) == 0:
- raise ValueError(
- f"Can't find GDAL shared library matching '{pattern}' in '{lib_dir}'"
- )
- if len(possible_files) > 1:
- # Pick the one with shortest name (usually the main library)
- possible_files.sort(key=lambda p: len(p.name))
-
- return possible_files[0]
-
-
def _configure_gdal_conda():
# Try CONDA_PREFIX first, then fall back to sys.prefix
conda_prefix = os.environ.get("CONDA_PREFIX")
@@ -1073,19 +993,7 @@ def _configure_gdal_conda():
# may not be set, but sys.prefix points to the environment
prefix = Path(sys.prefix)
- if not prefix.exists():
- raise ValueError(f"Can't configure GDAL from prefix '{prefix}': does not exist")
-
- if sys.platform == "win32":
- lib_dir = prefix / "Library" / "bin"
- else:
- lib_dir = prefix / "lib"
-
- if not lib_dir.exists():
- raise ValueError(f"Can't find GDAL library directory '{lib_dir}'")
-
- shared_library = _find_gdal_shared_library(lib_dir)
- configure_gdal(shared_library=shared_library)
+ configure_gdal_prefix_native(str(prefix))
def gdal_version() -> Optional[str]:
diff --git a/python/sedonadb/src/lib.rs b/python/sedonadb/src/lib.rs
index 0a49edbe9..5d5946ff7 100644
--- a/python/sedonadb/src/lib.rs
+++ b/python/sedonadb/src/lib.rs
@@ -21,6 +21,7 @@ use crate::{
udf::{sedona_aggregate_udf, sedona_native_scalar_udf, sedona_scalar_udf},
};
use pyo3::{exceptions::PyValueError, ffi::Py_uintptr_t, prelude::*};
+use sedona::native_library::NativeLibrary;
use sedona_adbc::AdbcSedonadbDriverInit;
use sedona_gdal::global::{configure_global_gdal_api, with_global_gdal, GdalApiBuilder};
use sedona_proj::register::{configure_global_proj_engine, ProjCrsEngineBuilder};
@@ -98,6 +99,9 @@ fn configure_proj_shared(
builder = builder.with_search_paths(vec![search_path.into()]);
}
+ builder
+ .build()
+ .map_err(|e| PySedonaError::SedonaPython(e.to_string()))?;
configure_global_proj_engine(builder)
.map_err(|e| PySedonaError::SedonaPython(e.to_string()))?;
Ok(())
@@ -106,12 +110,65 @@ fn configure_proj_shared(
#[pyfunction]
fn configure_gdal_shared(shared_library_path: String) -> Result<(), PySedonaError> {
let builder = GdalApiBuilder::default().with_shared_library(shared_library_path.into());
+ builder.build().map_err(|e| {
+ PySedonaError::SedonaPython(format!("Failed to load GDAL shared library: {e}"))
+ })?;
configure_global_gdal_api(builder).map_err(|e| {
PySedonaError::SedonaPython(format!("Failed to configure GDAL shared library: {e}"))
})?;
Ok(())
}
+#[pyfunction]
+fn configure_proj_prefix_native(prefix: String) -> Result<(), PySedonaError> {
+ let installation = NativeLibrary::Proj
+ .from_prefix(prefix.as_ref())
+ .map_err(PySedonaError::SedonaPython)?;
+ configure_proj_shared(
+ Some(installation.shared_library.to_string_lossy().into_owned()),
+ Some(
+ installation
+ .data_dir
+ .join("proj.db")
+ .to_string_lossy()
+ .into_owned(),
+ ),
+ Some(installation.data_dir.to_string_lossy().into_owned()),
+ )
+}
+
+#[pyfunction]
+fn configure_proj_system_native() -> Result<(), PySedonaError> {
+ let mut errors = Vec::new();
+ for candidate in NativeLibrary::Proj.system_candidates() {
+ match configure_proj_shared(Some(candidate.to_string_lossy().into_owned()), None, None) {
+ Ok(()) => return Ok(()),
+ Err(e) => errors.push(e.to_string()),
+ }
+ }
+ Err(PySedonaError::SedonaPython(errors.join("; ")))
+}
+
+#[pyfunction]
+fn configure_gdal_prefix_native(prefix: String) -> Result<(), PySedonaError> {
+ let installation = NativeLibrary::Gdal
+ .from_prefix(prefix.as_ref())
+ .map_err(PySedonaError::SedonaPython)?;
+ configure_gdal_shared(installation.shared_library.to_string_lossy().into_owned())
+}
+
+#[pyfunction]
+fn configure_gdal_system_native() -> Result<(), PySedonaError> {
+ let mut errors = Vec::new();
+ for candidate in NativeLibrary::Gdal.system_candidates() {
+ match configure_gdal_shared(candidate.to_string_lossy().into_owned()) {
+ Ok(()) => return Ok(()),
+ Err(e) => errors.push(e.to_string()),
+ }
+ }
+ Err(PySedonaError::SedonaPython(errors.join("; ")))
+}
+
/// Derive a north-up, GDAL-order geotransform from a spatial bounding box and
/// grid shape. Thin wrapper over `geotransform_from_bbox_and_spatial_shape` so
/// the bbox-to-transform math stays a single Rust source of truth.
@@ -156,7 +213,11 @@ fn _lib(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(begin_gdal_shutdown, m)?)?;
m.add_function(wrap_pyfunction!(configure_gdal_shared, m)?)?;
+ m.add_function(wrap_pyfunction!(configure_gdal_prefix_native, m)?)?;
+ m.add_function(wrap_pyfunction!(configure_gdal_system_native, m)?)?;
m.add_function(wrap_pyfunction!(configure_proj_shared, m)?)?;
+ m.add_function(wrap_pyfunction!(configure_proj_prefix_native, m)?)?;
+ m.add_function(wrap_pyfunction!(configure_proj_system_native, m)?)?;
m.add_function(wrap_pyfunction!(expr::expr_binary, m)?)?;
m.add_function(wrap_pyfunction!(expr::expr_col, m)?)?;
m.add_function(wrap_pyfunction!(expr::expr_lit, m)?)?;
diff --git a/r/sedonadb/DESCRIPTION b/r/sedonadb/DESCRIPTION
index d011449e9..50331dd37 100644
--- a/r/sedonadb/DESCRIPTION
+++ b/r/sedonadb/DESCRIPTION
@@ -8,7 +8,8 @@ Description: Provides bindings for Apache SedonaDB, a lightweight
License: Apache License (>= 2)
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
-SystemRequirements: Cargo (Rust's package manager), rustc, CMake, Git
+SystemRequirements: Cargo (Rust's package manager), rustc, CMake, Git, curl,
+ pkg-config, GEOS, GDAL (>= 3.5), PROJ (>= 6.2)
Depends: R (>= 4.1.0)
Suggests:
adbcdrivermanager,
diff --git a/r/sedonadb/NAMESPACE b/r/sedonadb/NAMESPACE
index c638a9cf8..45d955002 100644
--- a/r/sedonadb/NAMESPACE
+++ b/r/sedonadb/NAMESPACE
@@ -53,6 +53,7 @@ export(sd_anti_join)
export(sd_arrange)
export(sd_collect)
export(sd_compute)
+export(sd_configure_gdal)
export(sd_configure_proj)
export(sd_connect)
export(sd_count)
diff --git a/r/sedonadb/R/000-wrappers.R b/r/sedonadb/R/000-wrappers.R
index a1f99acd4..2d86a1535 100644
--- a/r/sedonadb/R/000-wrappers.R
+++ b/r/sedonadb/R/000-wrappers.R
@@ -85,6 +85,31 @@ NULL
}
+`configure_gdal_prefix_native` <- function(`prefix`) {
+ invisible(.Call(savvy_configure_gdal_prefix_native__impl, `prefix`))
+}
+
+
+`configure_gdal_shared` <- function(`shared_library_path` = NULL) {
+ invisible(.Call(savvy_configure_gdal_shared__impl, `shared_library_path`))
+}
+
+
+`configure_gdal_system_native` <- function() {
+ invisible(.Call(savvy_configure_gdal_system_native__impl))
+}
+
+
+`configure_proj_prefix_native` <- function(`prefix`) {
+ invisible(.Call(savvy_configure_proj_prefix_native__impl, `prefix`))
+}
+
+
+`configure_proj_system_native` <- function() {
+ invisible(.Call(savvy_configure_proj_system_native__impl))
+}
+
+
`init_r_runtime_interrupts` <- function(`interrupts_call`, `pkg_env`) {
invisible(.Call(savvy_init_r_runtime_interrupts__impl, `interrupts_call`, `pkg_env`))
}
diff --git a/r/sedonadb/R/context.R b/r/sedonadb/R/context.R
index 9fea789ab..701e5fd2e 100644
--- a/r/sedonadb/R/context.R
+++ b/r/sedonadb/R/context.R
@@ -429,6 +429,7 @@ global_ctx$ctx <- NULL
#' not linked at build time.
#'
#' @param preset One of:
+#' - `"conda"`: Look for PROJ in `CONDA_PREFIX`.
#' - `"homebrew"`: Look for PROJ installed by Homebrew. This is the easiest
#' option on MacOS.
#' - `"system"`: Look for PROJ in the platform library load path (e.g.,
@@ -456,6 +457,10 @@ sd_configure_proj <- function(
if (!is.null(preset)) {
switch(
preset,
+ conda = {
+ configure_proj_prefix(Sys.getenv("CONDA_PREFIX"))
+ return(invisible(NULL))
+ },
homebrew = {
configure_proj_prefix(Sys.getenv("HOMEBREW_PREFIX", "/opt/homebrew"))
return(invisible(NULL))
@@ -465,7 +470,7 @@ sd_configure_proj <- function(
return(invisible(NULL))
},
auto = {
- presets <- c("homebrew", "system")
+ presets <- c("conda", "homebrew", "system")
errors <- c()
for (preset in presets) {
maybe_err <- try(sd_configure_proj(preset), silent = TRUE)
@@ -513,30 +518,66 @@ sd_configure_proj <- function(
}
configure_proj_system <- function() {
- sd_configure_proj(shared_library = proj_dll_name())
+ configure_proj_system_native()
}
configure_proj_prefix <- function(prefix) {
- if (!dir.exists(prefix)) {
- stop(sprintf("Can't configure PROJ from prefix '%s': does not exist", prefix))
+ configure_proj_prefix_native(prefix)
+}
+
+#' Configure GDAL
+#'
+#' SedonaDB loads GDAL dynamically at runtime. Windows binary packages bundle
+#' the GDAL DLL and matching data files built by vcpkg. Automatic configuration
+#' is attempted when the package is loaded.
+#'
+#' @param preset One of `"conda"`, `"homebrew"`, `"system"`, or `"auto"`.
+#' @param shared_library A path or platform library name for GDAL.
+#'
+#' @returns NULL, invisibly.
+#' @export
+sd_configure_gdal <- function(preset = NULL, shared_library = NULL) {
+ if (!is.null(preset)) {
+ switch(
+ preset,
+ conda = {
+ configure_gdal_prefix_native(Sys.getenv("CONDA_PREFIX"))
+ return(invisible(NULL))
+ },
+ homebrew = {
+ configure_gdal_prefix_native(Sys.getenv("HOMEBREW_PREFIX", "/opt/homebrew"))
+ return(invisible(NULL))
+ },
+ system = {
+ configure_gdal_system_native()
+ return(invisible(NULL))
+ },
+ auto = {
+ presets <- c("conda", "homebrew", "system")
+ errors <- character()
+ for (candidate in presets) {
+ maybe_err <- try(sd_configure_gdal(candidate), silent = TRUE)
+ if (!inherits(maybe_err, "try-error")) {
+ return(invisible(NULL))
+ }
+ errors <- c(errors, sprintf("%s: %s", candidate, maybe_err))
+ }
+
+ packageStartupMessage(sprintf(
+ "Failed to configure GDAL (tried %s):\n%s",
+ paste0("'", presets, "'", collapse = ", "),
+ paste0(errors, collapse = "\n")
+ ))
+ return(invisible(NULL))
+ },
+ stop(sprintf("Unknown preset: '%s'", preset))
+ )
}
- sd_configure_proj(
- shared_library = file.path(prefix, "lib", proj_dll_name()),
- database_path = file.path(prefix, "share", "proj", "proj.db"),
- search_path = file.path(prefix, "share", "proj")
- )
-}
+ if (is.null(shared_library)) {
+ stop("Must provide `shared_library` or `preset`")
+ }
-proj_dll_name <- function() {
- switch(
- tolower(Sys.info()[["sysname"]]),
- windows = "proj.dll",
- darwin = "libproj.dylib",
- linux = "libproj.so",
- stop(sprintf(
- "Can't determine system PROJ shared library name for OS: %s",
- Sys.info()[["sysname"]]
- ))
- )
+ configure_gdal_shared(shared_library)
+ invisible(NULL)
}
diff --git a/r/sedonadb/R/zzz.R b/r/sedonadb/R/zzz.R
index 0eb13202c..82f23051b 100644
--- a/r/sedonadb/R/zzz.R
+++ b/r/sedonadb/R/zzz.R
@@ -25,6 +25,24 @@
ns <- asNamespace("sedonadb")
call <- call("check_interrupts")
init_r_runtime_interrupts(call, ns)
+
+ bundled_proj <- system.file("proj", package = "sedonadb")
+ if (nzchar(bundled_proj)) {
+ configure_proj_shared(
+ database_path = file.path(bundled_proj, "proj.db"),
+ search_path = bundled_proj
+ )
+ } else {
+ sd_configure_proj("auto")
+ }
+
+ bundled_gdal <- system.file("gdal", package = "sedonadb")
+ if (nzchar(bundled_gdal)) {
+ Sys.setenv(GDAL_DATA = bundled_gdal)
+ configure_gdal_shared()
+ } else {
+ sd_configure_gdal("auto")
+ }
}
# The function we call from Rust to check for interrupts. R checks for
diff --git a/r/sedonadb/README.Rmd b/r/sedonadb/README.Rmd
index fc3562c9a..dfbf6d046 100644
--- a/r/sedonadb/README.Rmd
+++ b/r/sedonadb/README.Rmd
@@ -53,7 +53,9 @@ You can install the development version of sedonadb from [GitHub](https://github
pak::pkg_install("apache/sedona-db/r/sedonadb")
```
-Installing a development version of sedonadb requires a [Rust compiler](https://rustup.rs), CMake, Git, and a GEOS system dependency (e.g., `brew install geos` or `apt-get install libgeos-dev`). Install instructions for GEOS on other platforms can be found on the [sf package homepage](https://r-spatial.github.io/sf). The install automatically sets up a pinned vcpkg checkout for the S2, Abseil, and OpenSSL dependencies. Set `SEDONADB_USE_VCPKG=false` to disable automatic setup and use dep [...]
+Installing a development version of sedonadb requires a [Rust compiler](https://rustup.rs), CMake, Git, pkg-config, curl, and GEOS. GDAL and PROJ shared libraries are also required to use raster and coordinate transformation functions. For example, on Ubuntu use `apt-get install libgeos-dev libgdal-dev libproj-dev pkg-config curl`, or on macOS use `brew install geos gdal proj pkg-config`.
+
+The Windows build automatically sets up a pinned vcpkg checkout and builds and statically links GDAL, PROJ, S2, Abseil, and OpenSSL with the Rtools compiler. The matching GDAL/PROJ data files are bundled in the installed package. Set `SEDONADB_USE_VCPKG=false` to disable automatic setup and provide a bootstrapped checkout through `VCPKG_ROOT` instead.
## Example
diff --git a/r/sedonadb/README.md b/r/sedonadb/README.md
index 5bdd7f7a3..7005d6aba 100644
--- a/r/sedonadb/README.md
+++ b/r/sedonadb/README.md
@@ -47,13 +47,18 @@ pak::pkg_install("apache/sedona-db/r/sedonadb")
```
Installing a development version of sedonadb requires a [Rust
-compiler](https://rustup.rs), CMake, Git, and a GEOS system dependency
-(e.g., `brew install geos` or `apt-get install libgeos-dev`). Install
-instructions for GEOS on other platforms can be found on the [sf package
-homepage](https://r-spatial.github.io/sf). The install automatically sets
-up a pinned vcpkg checkout for the S2, Abseil, and OpenSSL dependencies.
-Set `SEDONADB_USE_VCPKG=false` to disable automatic setup and use
-dependencies provided through `VCPKG_ROOT` or the system instead.
+compiler](https://rustup.rs), CMake, Git, pkg-config, curl, and GEOS. GDAL
+and PROJ shared libraries are also required to use raster and coordinate
+transformation functions. For example, on Ubuntu use `apt-get install
+libgeos-dev libgdal-dev libproj-dev pkg-config curl`, or on macOS use
+`brew install geos gdal proj pkg-config`.
+
+The Windows build automatically sets up a pinned vcpkg checkout and
+builds and statically links GDAL, PROJ, S2, Abseil, and OpenSSL with the
+Rtools compiler. The matching GDAL/PROJ data files are bundled in the
+installed package. Set
+`SEDONADB_USE_VCPKG=false` to disable automatic setup and provide a
+bootstrapped checkout through `VCPKG_ROOT` instead.
## Example
diff --git a/r/sedonadb/cleanup.win b/r/sedonadb/cleanup.win
index e81e481cb..5a9d2a1c0 100755
--- a/r/sedonadb/cleanup.win
+++ b/r/sedonadb/cleanup.win
@@ -16,3 +16,4 @@
# under the License.
rm -f src/Makevars.win
+rm -rf inst/gdal inst/proj
diff --git a/r/sedonadb/configure.win b/r/sedonadb/configure.win
index 090987185..cd865aa37 100755
--- a/r/sedonadb/configure.win
+++ b/r/sedonadb/configure.win
@@ -20,7 +20,7 @@
# available (even if it isn't used). Here we just try pkg-config (LIB_DIR
# can be used to specify the location of -lgeos_c)
-pkg-config geos 2>/dev/null
+pkg-config geos 2>/dev/null
if [ $? -eq 0 ]; then
PKGCONFIG_LIBS=`pkg-config --libs geos`
fi
@@ -105,9 +105,13 @@ fi
# Build spherical geography support by default. Setting SAVVY_FEATURES keeps
# its existing role as an explicit feature override.
if [ -z "${SAVVY_FEATURES+x}" ]; then
- SAVVY_FEATURES="s2geography"
+ SAVVY_FEATURES="s2geography gdal proj"
fi
+# Cargo accepts comma- or space-separated feature lists. Normalize to spaces so
+# the feature checks below handle both forms consistently.
+SAVVY_FEATURES="$(printf '%s' "${SAVVY_FEATURES}" | tr ',' ' ')"
+
# e.g. SAVVY_FEATURES="a b" --> "--features 'a b'"
if [ -n "${SAVVY_FEATURES}" ]; then
FEATURE_FLAGS="--features '${SAVVY_FEATURES}'"
@@ -127,70 +131,151 @@ case "$($CC -dumpmachine)" in
esac
echo "using Rust target: '${TARGET}'"
-# sedona-s2geography needs OpenSSL, Abseil, and s2geometry built with the same
-# MinGW/gnullvm compiler and flags as R. The usual vcpkg Windows triplets use
-# MSVC, whose C++ ABI is not compatible with Rtools.
+# gdal-sys ships pre-generated Windows bindings for x86_64 but not arm64.
+# Generate them from the vcpkg headers for the gnullvm target.
+case "${TARGET}" in
+ aarch64-*)
+ case " ${SAVVY_FEATURES} " in
+ *" gdal "*)
+ SAVVY_FEATURES="${SAVVY_FEATURES} bindgen"
+ FEATURE_FLAGS="--features '${SAVVY_FEATURES}'"
+ ;;
+ esac
+ ;;
+esac
+
+# Native C/C++ dependencies must use the same MinGW/gnullvm compiler and flags
+# as R. The usual vcpkg Windows triplets use MSVC, whose C++ ABI is not
+# compatible with Rtools.
S2_VCPKG_ROOT=""
S2_VCPKG_TARGET_TRIPLET=""
S2_VCPKG_OVERLAY_TRIPLETS=""
S2_CMAKE_TOOLCHAIN_FILE=""
S2_CMAKE_PREFIX_PATH=""
S2_OPENSSL_ROOT_DIR=""
-SAVVY_FEATURE_WORDS="$(printf '%s' "${SAVVY_FEATURES}" | tr ',' ' ')"
-case " ${SAVVY_FEATURE_WORDS} " in
- *" s2geography "*)
- case "${SEDONADB_USE_VCPKG:-true}" in
- 0|false|FALSE|no|NO|off|OFF) SEDONADB_SETUP_VCPKG=false ;;
- *) SEDONADB_SETUP_VCPKG=true ;;
- esac
+SPATIAL_VCPKG_PREFIX=""
+GDAL_VERSION=""
- if [ "${SEDONADB_SETUP_VCPKG}" = true ] && [ -z "${VCPKG_ROOT}" ]; then
- VCPKG_ROOT="$(sh tools/setup-vcpkg.sh)" || exit 1
- fi
-
- if [ -z "${VCPKG_ROOT}" ] || [ ! -x "${VCPKG_ROOT}/vcpkg.exe" ]; then
- echo "-------------- ERROR: CONFIGURATION FAILED --------------------"
- echo ""
- echo "Building with SAVVY_FEATURES=s2geography on Windows requires"
- echo "VCPKG_ROOT to point to a bootstrapped vcpkg checkout, or"
- echo "automatic setup via SEDONADB_USE_VCPKG (the default)."
- echo ""
- echo "---------------------------------------------------------------"
- exit 1
- fi
-
- VCPKG_ROOT="$(cygpath -m "${VCPKG_ROOT}")"
- CXX="$("${R_HOME}/bin/R" CMD config CXX17)"
- CPPFLAGS="$("${R_HOME}/bin/R" CMD config CPPFLAGS)"
- CFLAGS="$("${R_HOME}/bin/R" CMD config CFLAGS)"
- CXXFLAGS="$("${R_HOME}/bin/R" CMD config CXX17FLAGS)"
- LDFLAGS="$("${R_HOME}/bin/R" CMD config LDFLAGS)"
-
- case "${TARGET}" in
- aarch64-*) S2_VCPKG_TARGET_TRIPLET="sedonadb-arm64-mingw-static-release" ;;
- *) S2_VCPKG_TARGET_TRIPLET="sedonadb-x64-mingw-static-release" ;;
- esac
+NEEDS_S2=false
+NEEDS_GDAL=false
+NEEDS_PROJ=false
+case " ${SAVVY_FEATURES} " in
+ *" s2geography "*) NEEDS_S2=true ;;
+esac
+case " ${SAVVY_FEATURES} " in
+ *" gdal "*) NEEDS_GDAL=true ;;
+esac
+case " ${SAVVY_FEATURES} " in
+ *" proj "*) NEEDS_PROJ=true ;;
+esac
+
+if [ "${NEEDS_S2}" = true ] || [ "${NEEDS_GDAL}" = true ] || [ "${NEEDS_PROJ}" = true ]; then
+ case "${SEDONADB_USE_VCPKG:-true}" in
+ 0|false|FALSE|no|NO|off|OFF) SEDONADB_SETUP_VCPKG=false ;;
+ *) SEDONADB_SETUP_VCPKG=true ;;
+ esac
+
+ if [ "${SEDONADB_SETUP_VCPKG}" = true ] && [ -z "${VCPKG_ROOT}" ]; then
+ VCPKG_ROOT="$(sh tools/setup-vcpkg.sh)" || exit 1
+ fi
+
+ if [ -z "${VCPKG_ROOT}" ] || [ ! -x "${VCPKG_ROOT}/vcpkg.exe" ]; then
+ echo "-------------- ERROR: CONFIGURATION FAILED --------------------"
+ echo ""
+ echo "Building the requested native features on Windows requires"
+ echo "VCPKG_ROOT to point to a bootstrapped vcpkg checkout, or"
+ echo "automatic setup via SEDONADB_USE_VCPKG (the default)."
+ echo ""
+ echo "---------------------------------------------------------------"
+ exit 1
+ fi
+
+ VCPKG_ROOT="$(cygpath -m "${VCPKG_ROOT}")"
+ VCPKG_OVERLAY_TRIPLETS="$(pwd)/tools/vcpkg-triplets"
+ CXX="$("${R_HOME}/bin/R" CMD config CXX17)"
+ CPPFLAGS="$("${R_HOME}/bin/R" CMD config CPPFLAGS)"
+ CFLAGS="$("${R_HOME}/bin/R" CMD config CFLAGS)"
+ CXXFLAGS="$("${R_HOME}/bin/R" CMD config CXX17FLAGS)"
+ LDFLAGS="$("${R_HOME}/bin/R" CMD config LDFLAGS)"
+
+ export SEDONADB_R_CC="${CC}"
+ export SEDONADB_R_CXX="${CXX}"
+ export SEDONADB_R_CFLAGS="${CPPFLAGS} ${CFLAGS}"
+ export SEDONADB_R_CXXFLAGS="${CPPFLAGS} ${CXXFLAGS}"
+ export SEDONADB_R_LDFLAGS="${LDFLAGS}"
+fi
+
+if [ "${NEEDS_S2}" = true ]; then
+ case "${TARGET}" in
+ aarch64-*) S2_VCPKG_TARGET_TRIPLET="sedonadb-arm64-mingw-static-release" ;;
+ *) S2_VCPKG_TARGET_TRIPLET="sedonadb-x64-mingw-static-release" ;;
+ esac
+
+ S2_VCPKG_ROOT="${VCPKG_ROOT}"
+ echo "building s2geography dependencies with R's compiler (${CC}, ${CXX})"
+ "${VCPKG_ROOT}/vcpkg.exe" \
+ --overlay-triplets="${VCPKG_OVERLAY_TRIPLETS}" \
+ --host-triplet=x64-mingw-static \
+ --triplet="${S2_VCPKG_TARGET_TRIPLET}" \
+ install openssl abseil s2geometry || exit 1
+
+ S2_VCPKG_OVERLAY_TRIPLETS="$(cygpath -m "${VCPKG_OVERLAY_TRIPLETS}")"
+ S2_CMAKE_TOOLCHAIN_FILE="${S2_VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
+ S2_CMAKE_PREFIX_PATH="${S2_VCPKG_ROOT}/installed/${S2_VCPKG_TARGET_TRIPLET}"
+ S2_OPENSSL_ROOT_DIR="${S2_CMAKE_PREFIX_PATH}"
+fi
- S2_VCPKG_ROOT="${VCPKG_ROOT}"
- S2_VCPKG_OVERLAY_TRIPLETS="$(pwd)/tools/vcpkg-triplets"
- export SEDONADB_R_CC="${CC}"
- export SEDONADB_R_CXX="${CXX}"
- export SEDONADB_R_CFLAGS="${CPPFLAGS} ${CFLAGS}"
- export SEDONADB_R_CXXFLAGS="${CPPFLAGS} ${CXXFLAGS}"
- export SEDONADB_R_LDFLAGS="${LDFLAGS}"
+if [ "${NEEDS_GDAL}" = true ] || [ "${NEEDS_PROJ}" = true ]; then
+ case "${TARGET}" in
+ aarch64-*) SPATIAL_VCPKG_TARGET_TRIPLET="sedonadb-arm64-mingw-spatial-release" ;;
+ *) SPATIAL_VCPKG_TARGET_TRIPLET="sedonadb-x64-mingw-spatial-release" ;;
+ esac
- echo "building s2geography dependencies with R's compiler (${CC}, ${CXX})"
+ if [ "${NEEDS_GDAL}" = true ]; then
+ echo "building GDAL and PROJ with R's compiler (${CC}, ${CXX})"
"${VCPKG_ROOT}/vcpkg.exe" \
- --overlay-triplets="${S2_VCPKG_OVERLAY_TRIPLETS}" \
- --triplet="${S2_VCPKG_TARGET_TRIPLET}" \
- install openssl abseil s2geometry || exit 1
-
- S2_VCPKG_OVERLAY_TRIPLETS="$(cygpath -m "${S2_VCPKG_OVERLAY_TRIPLETS}")"
- S2_CMAKE_TOOLCHAIN_FILE="${S2_VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
- S2_CMAKE_PREFIX_PATH="${S2_VCPKG_ROOT}/installed/${S2_VCPKG_TARGET_TRIPLET}"
- S2_OPENSSL_ROOT_DIR="${S2_CMAKE_PREFIX_PATH}"
- ;;
-esac
+ --overlay-triplets="${VCPKG_OVERLAY_TRIPLETS}" \
+ --host-triplet=x64-mingw-static \
+ --triplet="${SPATIAL_VCPKG_TARGET_TRIPLET}" \
+ install "gdal[core]" || exit 1
+ else
+ echo "building PROJ with R's compiler (${CC}, ${CXX})"
+ "${VCPKG_ROOT}/vcpkg.exe" \
+ --overlay-triplets="${VCPKG_OVERLAY_TRIPLETS}" \
+ --host-triplet=x64-mingw-static \
+ --triplet="${SPATIAL_VCPKG_TARGET_TRIPLET}" \
+ install proj || exit 1
+ fi
+
+ SPATIAL_VCPKG_PREFIX="${VCPKG_ROOT}/installed/${SPATIAL_VCPKG_TARGET_TRIPLET}"
+ SPATIAL_PKG_CONFIG_PREFIX="$(cygpath -u "${SPATIAL_VCPKG_PREFIX}")"
+ SPATIAL_PKG_CONFIG_LIBDIR="${SPATIAL_PKG_CONFIG_PREFIX}/lib/pkgconfig:${SPATIAL_PKG_CONFIG_PREFIX}/share/pkgconfig"
+ if [ "${NEEDS_GDAL}" = true ]; then
+ GDAL_VERSION="$(awk '
+ /#define GDAL_VERSION_MAJOR/ { major=$3 }
+ /#define GDAL_VERSION_MINOR/ { minor=$3 }
+ /#define GDAL_VERSION_REV/ { patch=$3 }
+ END { print major "." minor "." patch }
+ ' "${SPATIAL_VCPKG_PREFIX}/include/gdal_version.h")"
+ SPATIAL_LIBS_RAW="$(PKG_CONFIG_PATH= PKG_CONFIG_LIBDIR="${SPATIAL_PKG_CONFIG_LIBDIR}" PKG_CONFIG_SYSROOT_DIR= pkg-config --static --libs gdal)" || exit 1
+
+ # gdal-sys bundles libgdal.a into libsedonadbr.a, but a Rust staticlib
+ # does not propagate GDAL's transitive native link requirements to R.
+ # Keep the vcpkg dependency closure while removing the duplicate archive.
+ SPATIAL_LIBS=""
+ for SPATIAL_LIB in ${SPATIAL_LIBS_RAW}; do
+ if [ "${SPATIAL_LIB}" != "-lgdal" ]; then
+ SPATIAL_LIBS="${SPATIAL_LIBS} ${SPATIAL_LIB}"
+ fi
+ done
+ else
+ SPATIAL_LIBS="$(PKG_CONFIG_PATH= PKG_CONFIG_LIBDIR="${SPATIAL_PKG_CONFIG_LIBDIR}" PKG_CONFIG_SYSROOT_DIR= pkg-config --static --libs proj)" || exit 1
+ fi
+
+ # Rescan the static dependency closure because GDAL and PROJ contain cycles.
+ # The package-level GEOS dependency remains separate and follows the group.
+ PKG_LIBS="-Wl,--start-group ${SPATIAL_LIBS} -Wl,--end-group ${PKGCONFIG_LIBS:--lgeos_c -lgeos}"
+fi
sed \
-e "s/@TARGET@/${TARGET}/" \
@@ -203,5 +288,7 @@ sed \
-e "s|@CMAKE_TOOLCHAIN_FILE@|${S2_CMAKE_TOOLCHAIN_FILE}|" \
-e "s|@CMAKE_PREFIX_PATH@|${S2_CMAKE_PREFIX_PATH}|" \
-e "s|@OPENSSL_ROOT_DIR@|${S2_OPENSSL_ROOT_DIR}|" \
+ -e "s|@SPATIAL_VCPKG_PREFIX@|${SPATIAL_VCPKG_PREFIX}|" \
+ -e "s|@GDAL_VERSION@|${GDAL_VERSION}|" \
-e "s|@PKG_LIBS@|${PKG_LIBS}|" \
src/Makevars.win.in > src/Makevars.win
diff --git a/r/sedonadb/man/sd_configure_gdal.Rd b/r/sedonadb/man/sd_configure_gdal.Rd
new file mode 100644
index 000000000..b4ad9269e
--- /dev/null
+++ b/r/sedonadb/man/sd_configure_gdal.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/context.R
+\name{sd_configure_gdal}
+\alias{sd_configure_gdal}
+\title{Configure GDAL}
+\usage{
+sd_configure_gdal(preset = NULL, shared_library = NULL)
+}
+\arguments{
+\item{preset}{One of \code{"conda"}, \code{"homebrew"}, \code{"system"}, or \code{"auto"}.}
+
+\item{shared_library}{A path or platform library name for GDAL.}
+}
+\value{
+NULL, invisibly.
+}
+\description{
+SedonaDB loads GDAL dynamically at runtime. Windows binary packages bundle
+the GDAL DLL and matching data files built by vcpkg. Automatic configuration
+is attempted when the package is loaded.
+}
diff --git a/r/sedonadb/man/sd_configure_proj.Rd b/r/sedonadb/man/sd_configure_proj.Rd
index 2ddb2b179..cf92eecdc 100644
--- a/r/sedonadb/man/sd_configure_proj.Rd
+++ b/r/sedonadb/man/sd_configure_proj.Rd
@@ -14,6 +14,7 @@ sd_configure_proj(
\arguments{
\item{preset}{One of:
\itemize{
+\item \code{"conda"}: Look for PROJ in \code{CONDA_PREFIX}.
\item \code{"homebrew"}: Look for PROJ installed by Homebrew. This is the easiest
option on MacOS.
\item \code{"system"}: Look for PROJ in the platform library load path (e.g.,
diff --git a/r/sedonadb/src/Makevars.win.in b/r/sedonadb/src/Makevars.win.in
index 20c6cbdf1..cf093b27d 100644
--- a/r/sedonadb/src/Makevars.win.in
+++ b/r/sedonadb/src/Makevars.win.in
@@ -27,6 +27,8 @@ VCPKG_MANIFEST_MODE = OFF
CMAKE_TOOLCHAIN_FILE = @CMAKE_TOOLCHAIN_FILE@
CMAKE_PREFIX_PATH = @CMAKE_PREFIX_PATH@
OPENSSL_ROOT_DIR = @OPENSSL_ROOT_DIR@
+SPATIAL_VCPKG_PREFIX = @SPATIAL_VCPKG_PREFIX@
+GDAL_VERSION = @GDAL_VERSION@
SEDONADB_R_CC = $(strip $(CC))
SEDONADB_R_CXX = $(strip $(CXX17))
SEDONADB_R_CFLAGS = $(CPPFLAGS) $(CFLAGS)
@@ -81,7 +83,7 @@ PKG_CONFIG_SYSROOT_DIR = /x86_64-w64-mingw32.static.posix/
# also incompatible with aws-lc-sys.
CFLAGS_TWEAKED = $(filter-out -O%,$(CFLAGS)) -Wno-pedantic
-all: $(SHLIB) clean_intermediate
+all: $(SHLIB) copy_native_data clean_intermediate
$(SHLIB): $(STATLIB)
@@ -94,6 +96,16 @@ $(STATLIB):
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" && \
export LIBRARY_PATH="$${LIBRARY_PATH};$(LIBDIR)/libgcc_mock" && \
export PKG_CONFIG_SYSROOT_DIR="$(PKG_CONFIG_SYSROOT_DIR)" && \
+ if [ -n "$(SPATIAL_VCPKG_PREFIX)" ]; then \
+ export PKG_CONFIG_ALLOW_CROSS=1; \
+ export PKG_CONFIG_ALL_STATIC=1; \
+ export PKG_CONFIG_SYSROOT_DIR=""; \
+ export PKG_CONFIG_PATH="$(SPATIAL_VCPKG_PREFIX)/lib/pkgconfig:$(SPATIAL_VCPKG_PREFIX)/share/pkgconfig$${PKG_CONFIG_PATH:+:$${PKG_CONFIG_PATH}}"; \
+ export GDAL_STATIC=1; \
+ export GDAL_INCLUDE_DIR="$(SPATIAL_VCPKG_PREFIX)/include"; \
+ export GDAL_LIB_DIR="$(SPATIAL_VCPKG_PREFIX)/lib"; \
+ export GDAL_VERSION="$(GDAL_VERSION)"; \
+ fi && \
if [ -n "$(VCPKG_ROOT)" ]; then \
export VCPKG_ROOT="$(VCPKG_ROOT)"; \
export VCPKG_DEFAULT_TRIPLET="$(VCPKG_TARGET_TRIPLET)"; \
@@ -119,7 +131,18 @@ $(STATLIB):
clean_intermediate: $(SHLIB)
rm -f $(STATLIB)
+# GDAL and PROJ runtime data must match the vcpkg libraries bundled above.
+# Copying these trees follows the Windows packaging pattern used by sf/vapour.
+copy_native_data:
+ if [ -n "$(SPATIAL_VCPKG_PREFIX)" ]; then \
+ test -d "$(SPATIAL_VCPKG_PREFIX)/share/gdal"; \
+ test -d "$(SPATIAL_VCPKG_PREFIX)/share/proj"; \
+ rm -rf ../inst/gdal ../inst/proj; \
+ cp -R "$(SPATIAL_VCPKG_PREFIX)/share/gdal" ../inst/gdal; \
+ cp -R "$(SPATIAL_VCPKG_PREFIX)/share/proj" ../inst/proj; \
+ fi
+
clean:
rm -Rf $(SHLIB) $(OBJECTS) $(STATLIB) ./rust/target
-.PHONY: all clean_intermediate clean
+.PHONY: all copy_native_data clean_intermediate clean
diff --git a/r/sedonadb/src/init.c b/r/sedonadb/src/init.c
index 5a295dc57..c12dedb3d 100644
--- a/r/sedonadb/src/init.c
+++ b/r/sedonadb/src/init.c
@@ -73,6 +73,27 @@ SEXP savvy_configure_proj_shared__impl(SEXP c_arg__shared_library_path,
return handle_result(res);
}
+SEXP savvy_configure_gdal_prefix_native__impl(SEXP c_arg__prefix) {
+ return handle_result(savvy_configure_gdal_prefix_native__ffi(c_arg__prefix));
+}
+
+SEXP savvy_configure_gdal_shared__impl(SEXP c_arg__shared_library_path) {
+ return handle_result(
+ savvy_configure_gdal_shared__ffi(c_arg__shared_library_path));
+}
+
+SEXP savvy_configure_gdal_system_native__impl(void) {
+ return handle_result(savvy_configure_gdal_system_native__ffi());
+}
+
+SEXP savvy_configure_proj_prefix_native__impl(SEXP c_arg__prefix) {
+ return handle_result(savvy_configure_proj_prefix_native__ffi(c_arg__prefix));
+}
+
+SEXP savvy_configure_proj_system_native__impl(void) {
+ return handle_result(savvy_configure_proj_system_native__ffi());
+}
+
SEXP savvy_init_r_runtime__impl(DllInfo *c_arg___dll_info) {
SEXP res = savvy_init_r_runtime__ffi(c_arg___dll_info);
return handle_result(res);
@@ -404,6 +425,16 @@ static const R_CallMethodDef CallEntries[] = {
(DL_FUNC)&savvy_apply_crses_to_sf_stream__impl, 4},
{"savvy_configure_proj_shared__impl",
(DL_FUNC)&savvy_configure_proj_shared__impl, 3},
+ {"savvy_configure_gdal_prefix_native__impl",
+ (DL_FUNC)&savvy_configure_gdal_prefix_native__impl, 1},
+ {"savvy_configure_gdal_shared__impl",
+ (DL_FUNC)&savvy_configure_gdal_shared__impl, 1},
+ {"savvy_configure_gdal_system_native__impl",
+ (DL_FUNC)&savvy_configure_gdal_system_native__impl, 0},
+ {"savvy_configure_proj_prefix_native__impl",
+ (DL_FUNC)&savvy_configure_proj_prefix_native__impl, 1},
+ {"savvy_configure_proj_system_native__impl",
+ (DL_FUNC)&savvy_configure_proj_system_native__impl, 0},
{"savvy_init_r_runtime_interrupts__impl",
(DL_FUNC)&savvy_init_r_runtime_interrupts__impl, 2},
{"savvy_sedonadb_adbc_init_func__impl",
diff --git a/r/sedonadb/cleanup.win b/r/sedonadb/src/install.libs.R
old mode 100755
new mode 100644
similarity index 69%
copy from r/sedonadb/cleanup.win
copy to r/sedonadb/src/install.libs.R
index e81e481cb..63784e17e
--- a/r/sedonadb/cleanup.win
+++ b/r/sedonadb/src/install.libs.R
@@ -15,4 +15,14 @@
# specific language governing permissions and limitations
# under the License.
-rm -f src/Makevars.win
+destination <- file.path(R_PACKAGE_DIR, paste0("libs", R_ARCH))
+dir.create(destination, recursive = TRUE, showWarnings = FALSE)
+
+package_dll <- Sys.glob(paste0("*", SHLIB_EXT))
+
+if (
+ length(package_dll) == 0L ||
+ !all(file.copy(package_dll, destination, overwrite = TRUE))
+) {
+ stop("Failed to install the sedonadb native library")
+}
diff --git a/r/sedonadb/src/rust/Cargo.toml b/r/sedonadb/src/rust/Cargo.toml
index 8301bf93a..490f28f04 100644
--- a/r/sedonadb/src/rust/Cargo.toml
+++ b/r/sedonadb/src/rust/Cargo.toml
@@ -26,6 +26,9 @@ crate-type = ["staticlib", "lib"]
[features]
s2geography = ["sedona/s2geography"]
+gdal = ["sedona/gdal"]
+bindgen = ["sedona/bindgen"]
+proj = ["sedona/proj"]
[dependencies]
arrow-array = { workspace = true }
@@ -43,6 +46,7 @@ sedona-expr = { workspace = true }
sedona-extension = { workspace = true }
sedona-geometry = { workspace = true }
sedona-geoparquet = { workspace = true }
+sedona-gdal = { workspace = true }
sedona-proj = { workspace = true }
sedona-schema = { workspace = true }
thiserror = { workspace = true }
diff --git a/r/sedonadb/src/rust/api.h b/r/sedonadb/src/rust/api.h
index 602bf959f..7eec679c7 100644
--- a/r/sedonadb/src/rust/api.h
+++ b/r/sedonadb/src/rust/api.h
@@ -22,6 +22,11 @@ SEXP savvy_apply_crses_to_sf_stream__ffi(SEXP c_arg__stream_in_xptr,
SEXP savvy_configure_proj_shared__ffi(SEXP c_arg__shared_library_path,
SEXP c_arg__database_path,
SEXP c_arg__search_path);
+SEXP savvy_configure_gdal_prefix_native__ffi(SEXP c_arg__prefix);
+SEXP savvy_configure_gdal_shared__ffi(SEXP c_arg__shared_library_path);
+SEXP savvy_configure_gdal_system_native__ffi(void);
+SEXP savvy_configure_proj_prefix_native__ffi(SEXP c_arg__prefix);
+SEXP savvy_configure_proj_system_native__ffi(void);
SEXP savvy_init_r_runtime__ffi(DllInfo *c_arg___dll_info);
SEXP savvy_init_r_runtime_interrupts__ffi(SEXP c_arg__interrupts_call,
SEXP c_arg__pkg_env);
diff --git a/r/sedonadb/src/rust/src/lib.rs b/r/sedonadb/src/rust/src/lib.rs
index 0db3f2b75..3ce19bb12 100644
--- a/r/sedonadb/src/rust/src/lib.rs
+++ b/r/sedonadb/src/rust/src/lib.rs
@@ -20,7 +20,9 @@ use std::ffi::c_void;
use savvy::savvy;
use savvy_ffi::R_NilValue;
+use sedona::native_library::NativeLibrary;
use sedona_adbc::AdbcSedonadbDriverInit;
+use sedona_gdal::global::{configure_global_gdal_api, GdalApiBuilder};
use sedona_proj::register::{configure_global_proj_engine, ProjCrsEngineBuilder};
mod context;
@@ -64,6 +66,68 @@ fn configure_proj_shared(
builder = builder.with_search_paths(vec![search_path.into()]);
}
+ builder.build()?;
configure_global_proj_engine(builder)?;
Ok(())
}
+
+#[savvy]
+fn configure_proj_prefix_native(prefix: &str) -> savvy::Result<()> {
+ let installation = NativeLibrary::Proj
+ .from_prefix(prefix.as_ref())
+ .map_err(savvy::Error::GeneralError)?;
+ configure_proj_shared(
+ Some(installation.shared_library.to_string_lossy().as_ref()),
+ Some(
+ installation
+ .data_dir
+ .join("proj.db")
+ .to_string_lossy()
+ .as_ref(),
+ ),
+ Some(installation.data_dir.to_string_lossy().as_ref()),
+ )
+}
+
+#[savvy]
+fn configure_proj_system_native() -> savvy::Result<()> {
+ let mut errors = Vec::new();
+ for candidate in NativeLibrary::Proj.system_candidates() {
+ match configure_proj_shared(Some(candidate.to_string_lossy().as_ref()), None, None) {
+ Ok(()) => return Ok(()),
+ Err(e) => errors.push(e.to_string()),
+ }
+ }
+ Err(savvy::Error::GeneralError(errors.join("; ")))
+}
+
+#[savvy]
+fn configure_gdal_shared(shared_library_path: Option<&str>) -> savvy::Result<()> {
+ let mut builder = GdalApiBuilder::default();
+ if let Some(shared_library_path) = shared_library_path {
+ builder = builder.with_shared_library(shared_library_path.into());
+ }
+ builder.build()?;
+ configure_global_gdal_api(builder)?;
+ Ok(())
+}
+
+#[savvy]
+fn configure_gdal_prefix_native(prefix: &str) -> savvy::Result<()> {
+ let installation = NativeLibrary::Gdal
+ .from_prefix(prefix.as_ref())
+ .map_err(savvy::Error::GeneralError)?;
+ configure_gdal_shared(Some(installation.shared_library.to_string_lossy().as_ref()))
+}
+
+#[savvy]
+fn configure_gdal_system_native() -> savvy::Result<()> {
+ let mut errors = Vec::new();
+ for candidate in NativeLibrary::Gdal.system_candidates() {
+ match configure_gdal_shared(Some(candidate.to_string_lossy().as_ref())) {
+ Ok(()) => return Ok(()),
+ Err(e) => errors.push(e.to_string()),
+ }
+ }
+ Err(savvy::Error::GeneralError(errors.join("; ")))
+}
diff --git a/r/sedonadb/tests/testthat/test-context.R b/r/sedonadb/tests/testthat/test-context.R
index 6eb692e8b..7fa327855 100644
--- a/r/sedonadb/tests/testthat/test-context.R
+++ b/r/sedonadb/tests/testthat/test-context.R
@@ -201,6 +201,18 @@ test_that("configure_proj() errors for invalid inputs", {
)
})
+test_that("configure_gdal() errors for invalid inputs", {
+ expect_error(
+ sd_configure_gdal("not a preset"),
+ "Unknown preset"
+ )
+
+ expect_error(
+ sd_configure_gdal(shared_library = "file that does not exist"),
+ "Failed to load GDAL"
+ )
+})
+
test_that(".fns can have its contents listed", {
expect_contains(names(.fns), "st_intersects")
expect_contains(.DollarNames(.fns, "st_int"), "st_intersects")
diff --git a/r/sedonadb/cleanup.win b/r/sedonadb/tools/vcpkg-triplets/sedonadb-arm64-mingw-spatial-release.cmake
old mode 100755
new mode 100644
similarity index 84%
copy from r/sedonadb/cleanup.win
copy to r/sedonadb/tools/vcpkg-triplets/sedonadb-arm64-mingw-spatial-release.cmake
index e81e481cb..ec1099fe7
--- a/r/sedonadb/cleanup.win
+++ b/r/sedonadb/tools/vcpkg-triplets/sedonadb-arm64-mingw-spatial-release.cmake
@@ -15,4 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-rm -f src/Makevars.win
+set(VCPKG_TARGET_ARCHITECTURE arm64)
+set(VCPKG_LIBRARY_LINKAGE static)
+
+include("${CMAKE_CURRENT_LIST_DIR}/sedonadb-mingw-static-release.cmake")
diff --git a/r/sedonadb/tools/vcpkg-triplets/sedonadb-mingw-static-release.cmake b/r/sedonadb/tools/vcpkg-triplets/sedonadb-mingw-static-release.cmake
index 7dd44e70c..0db28e1cc 100644
--- a/r/sedonadb/tools/vcpkg-triplets/sedonadb-mingw-static-release.cmake
+++ b/r/sedonadb/tools/vcpkg-triplets/sedonadb-mingw-static-release.cmake
@@ -16,7 +16,9 @@
# under the License.
set(VCPKG_CRT_LINKAGE dynamic)
-set(VCPKG_LIBRARY_LINKAGE static)
+if(NOT DEFINED VCPKG_LIBRARY_LINKAGE)
+ set(VCPKG_LIBRARY_LINKAGE static)
+endif()
set(VCPKG_BUILD_TYPE release)
set(VCPKG_CMAKE_SYSTEM_NAME MinGW)
diff --git a/r/sedonadb/cleanup.win b/r/sedonadb/tools/vcpkg-triplets/sedonadb-x64-mingw-spatial-release.cmake
old mode 100755
new mode 100644
similarity index 84%
copy from r/sedonadb/cleanup.win
copy to r/sedonadb/tools/vcpkg-triplets/sedonadb-x64-mingw-spatial-release.cmake
index e81e481cb..d0fdcb793
--- a/r/sedonadb/cleanup.win
+++ b/r/sedonadb/tools/vcpkg-triplets/sedonadb-x64-mingw-spatial-release.cmake
@@ -15,4 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-rm -f src/Makevars.win
+set(VCPKG_TARGET_ARCHITECTURE x64)
+set(VCPKG_LIBRARY_LINKAGE static)
+
+include("${CMAKE_CURRENT_LIST_DIR}/sedonadb-mingw-static-release.cmake")
diff --git a/rust/sedona/src/lib.rs b/rust/sedona/src/lib.rs
index 907848843..6e15c3f8f 100644
--- a/rust/sedona/src/lib.rs
+++ b/rust/sedona/src/lib.rs
@@ -20,6 +20,7 @@ pub mod context;
pub mod context_builder;
mod exec;
pub mod memory_pool;
+pub mod native_library;
mod object_storage;
pub use object_storage::is_object_store_option;
pub mod pool_type;
diff --git a/rust/sedona/src/native_library.rs b/rust/sedona/src/native_library.rs
new file mode 100644
index 000000000..520605bac
--- /dev/null
+++ b/rust/sedona/src/native_library.rs
@@ -0,0 +1,210 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Shared runtime discovery for native GDAL and PROJ installations.
+//!
+//! Language bindings may add package-specific discovery (for example, Python
+//! wheels bundled by rasterio or pyproj), but prefix-based installations such
+//! as Conda and Homebrew should resolve identically in every binding.
+
+use std::fmt::{Display, Formatter};
+use std::path::{Path, PathBuf};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum NativeLibrary {
+ Gdal,
+ Proj,
+}
+
+impl Display for NativeLibrary {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Gdal => f.write_str("GDAL"),
+ Self::Proj => f.write_str("PROJ"),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct NativeLibraryInstallation {
+ pub shared_library: PathBuf,
+ pub data_dir: PathBuf,
+}
+
+impl NativeLibrary {
+ pub fn system_library_name(self) -> &'static str {
+ self.system_library_names()[0]
+ }
+
+ pub fn system_library_names(self) -> &'static [&'static str] {
+ match (self, std::env::consts::OS) {
+ (Self::Gdal, "windows") => &["gdal.dll"],
+ (Self::Gdal, "macos") => &["libgdal.dylib"],
+ (Self::Gdal, _) => &["libgdal.so"],
+ (Self::Proj, "windows") => &["proj.dll", "proj_9.dll"],
+ (Self::Proj, "macos") => &["libproj.dylib"],
+ (Self::Proj, _) => &["libproj.so"],
+ }
+ }
+
+ /// Return loadable names and discovered files in platform library paths.
+ ///
+ /// The directory scan matters on Linux distributions that install only a
+ /// versioned runtime library (for example `libgdal.so.34`) unless the
+ /// separate development package is installed.
+ pub fn system_candidates(self) -> Vec<PathBuf> {
+ let mut candidates = self
+ .system_library_names()
+ .iter()
+ .map(PathBuf::from)
+ .collect::<Vec<_>>();
+
+ let search_var = match std::env::consts::OS {
+ "windows" => "PATH",
+ "macos" => "DYLD_LIBRARY_PATH",
+ _ => "LD_LIBRARY_PATH",
+ };
+ let mut search_dirs = std::env::var_os(search_var)
+ .map(|paths| std::env::split_paths(&paths).collect::<Vec<_>>())
+ .unwrap_or_default();
+
+ if cfg!(target_os = "linux") {
+ search_dirs.extend([
+ PathBuf::from("/usr/local/lib"),
+ PathBuf::from("/usr/lib"),
+ PathBuf::from("/lib"),
+ PathBuf::from(format!("/usr/lib/{}-linux-gnu", std::env::consts::ARCH)),
+ PathBuf::from(format!("/lib/{}-linux-gnu", std::env::consts::ARCH)),
+ ]);
+ } else if cfg!(target_os = "macos") {
+ search_dirs.extend([
+ PathBuf::from("/opt/homebrew/lib"),
+ PathBuf::from("/usr/local/lib"),
+ ]);
+ }
+
+ for dir in search_dirs {
+ if let Ok(path) = self.find_shared_library(&dir) {
+ if !candidates.contains(&path) {
+ candidates.push(path);
+ }
+ }
+ }
+
+ candidates
+ }
+
+ fn glob_parts(self) -> (&'static str, &'static str) {
+ match (self, std::env::consts::OS) {
+ (Self::Gdal, "windows") => ("gdal", ".dll"),
+ (Self::Gdal, "macos") => ("libgdal", ".dylib"),
+ (Self::Gdal, _) => ("libgdal.so", ""),
+ (Self::Proj, "windows") => ("proj", ".dll"),
+ (Self::Proj, "macos") => ("libproj", ".dylib"),
+ (Self::Proj, _) => ("libproj.so", ""),
+ }
+ }
+
+ pub fn from_prefix(self, prefix: &Path) -> Result<NativeLibraryInstallation, String> {
+ if prefix.as_os_str().is_empty() || !prefix.is_dir() {
+ return Err(format!(
+ "Can't configure {self} from prefix '{}': does not exist",
+ prefix.display()
+ ));
+ }
+
+ let (lib_dir, share_dir) = if cfg!(windows) {
+ (prefix.join("Library/bin"), prefix.join("Library/share"))
+ } else {
+ (prefix.join("lib"), prefix.join("share"))
+ };
+
+ let data_dir = share_dir.join(match self {
+ Self::Gdal => "gdal",
+ Self::Proj => "proj",
+ });
+ if !data_dir.is_dir() {
+ return Err(format!(
+ "Can't find {self} data directory '{}'",
+ data_dir.display()
+ ));
+ }
+
+ Ok(NativeLibraryInstallation {
+ shared_library: self.find_shared_library(&lib_dir)?,
+ data_dir,
+ })
+ }
+
+ pub fn find_shared_library(self, lib_dir: &Path) -> Result<PathBuf, String> {
+ let (prefix, suffix) = self.glob_parts();
+ let entries = std::fs::read_dir(lib_dir).map_err(|e| {
+ format!(
+ "Can't read {self} library directory '{}': {e}",
+ lib_dir.display()
+ )
+ })?;
+
+ let mut matches = entries
+ .filter_map(Result::ok)
+ .map(|entry| entry.path())
+ .filter(|path| {
+ let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
+ return false;
+ };
+ name.starts_with(prefix)
+ && (suffix.is_empty() || name.ends_with(suffix))
+ && !name.contains("_d.")
+ })
+ .collect::<Vec<_>>();
+
+ matches.sort_by_key(|path| {
+ path.file_name()
+ .map(|name| name.len())
+ .unwrap_or(usize::MAX)
+ });
+ matches.into_iter().next().ok_or_else(|| {
+ format!(
+ "Can't find {self} shared library in '{}'",
+ lib_dir.display()
+ )
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn system_names_match_platform() {
+ let gdal = NativeLibrary::Gdal.system_library_name();
+ let proj = NativeLibrary::Proj.system_library_name();
+ assert!(gdal.to_ascii_lowercase().contains("gdal"));
+ assert!(proj.to_ascii_lowercase().contains("proj"));
+ assert!(!NativeLibrary::Gdal.system_candidates().is_empty());
+ }
+
+ #[test]
+ fn missing_prefix_has_useful_error() {
+ let err = NativeLibrary::Proj
+ .from_prefix(Path::new("/path/that/does/not/exist"))
+ .unwrap_err();
+ assert!(err.contains("PROJ"));
+ assert!(err.contains("does not exist"));
+ }
+}