Skip to content

Commit b7432e4

Browse files
paleolimbotCopilot
andauthored
feat(r/sedonadb): Add support for runtime linking of PROJ (#166)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent a93b322 commit b7432e4

File tree

10 files changed

+221
-0
lines changed

10 files changed

+221
-0
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

r/sedonadb/NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ S3method(print,sedonadb_dataframe)
1919
export(as_sedonadb_dataframe)
2020
export(sd_collect)
2121
export(sd_compute)
22+
export(sd_configure_proj)
2223
export(sd_count)
2324
export(sd_drop_view)
2425
export(sd_preview)

r/sedonadb/R/000-wrappers.R

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ NULL
5555
}
5656

5757

58+
`configure_proj_shared` <- function(`shared_library_path` = NULL, `database_path` = NULL, `search_path` = NULL) {
59+
invisible(.Call(savvy_configure_proj_shared__impl, `shared_library_path`, `database_path`, `search_path`))
60+
}
61+
62+
5863
`init_r_runtime_interrupts` <- function(`interrupts_call`, `pkg_env`) {
5964
invisible(.Call(savvy_init_r_runtime_interrupts__impl, `interrupts_call`, `pkg_env`))
6065
}

r/sedonadb/R/context.R

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,117 @@ ctx <- function() {
9393

9494
global_ctx <- new.env(parent = emptyenv())
9595
global_ctx$ctx <- NULL
96+
97+
98+
99+
#' Configure PROJ
100+
#'
101+
#' Performs a runtime configuration of PROJ, which can be used in place of
102+
#' a build-time linked version of PROJ or to add in support if PROJ was
103+
#' not linked at build time.
104+
#'
105+
#' @param preset One of:
106+
#' - `"homebrew"`: Look for PROJ installed by Homebrew. This is the easiest
107+
#' option on MacOS.
108+
#' - `"system"`: Look for PROJ in the platform library load path (e.g.,
109+
#' after installing system proj on Linux).
110+
#' - `"auto"`: Try all presets in the order listed above, issuing a warning
111+
#' if none can be configured.
112+
#' @param shared_library An absolute or relative path to a shared library
113+
#' valid for the platform.
114+
#' @param database_path A path to proj.db
115+
#' @param search_path A path to the data files required by PROJ for some
116+
#' transforms.
117+
#'
118+
#' @returns NULL, invisibly
119+
#' @export
120+
#'
121+
#' @examples
122+
#' sd_configure_proj("auto")
123+
#'
124+
sd_configure_proj <- function(preset = NULL,
125+
shared_library = NULL,
126+
database_path = NULL,
127+
search_path = NULL) {
128+
if (!is.null(preset)) {
129+
switch (preset,
130+
homebrew = {
131+
configure_proj_prefix(Sys.getenv("HOMEBREW_PREFIX", "/opt/homebrew"))
132+
return(invisible(NULL))
133+
},
134+
system = {
135+
configure_proj_system()
136+
return(invisible(NULL))
137+
},
138+
auto = {
139+
presets <- c("homebrew", "system")
140+
errors <- c()
141+
for (preset in presets) {
142+
maybe_err <- try(sd_configure_proj(preset), silent = TRUE)
143+
if (!inherits(maybe_err, "try-error")) {
144+
return(invisible(NULL))
145+
} else {
146+
errors <- c(errors, sprintf("%s: %s", preset, maybe_err))
147+
}
148+
}
149+
150+
packageStartupMessage(
151+
sprintf(
152+
"Failed to configure PROJ (tried %s):\n%s",
153+
paste0("'", presets, "'", collapse = ", "),
154+
paste0(errors, collapse = "\n")
155+
)
156+
)
157+
158+
return(invisible(NULL))
159+
},
160+
stop(sprintf("Unknown preset: '%s'", preset))
161+
)
162+
}
163+
164+
# We could check a shared library with dyn.load(), but this may error for
165+
# valid system PROJ that isn't an absolute filename.
166+
167+
if (!is.null(database_path)) {
168+
if (!file.exists(database_path)) {
169+
stop(sprintf("Invalid database path: '%s' does not exist", database_path))
170+
}
171+
}
172+
173+
if (!is.null(search_path)) {
174+
if (!dir.exists(search_path)) {
175+
stop(sprintf("Invalid search path: '%s' does not exist", search_path))
176+
}
177+
}
178+
179+
configure_proj_shared(
180+
shared_library_path = shared_library,
181+
database_path = database_path,
182+
search_path = search_path
183+
)
184+
}
185+
186+
configure_proj_system <- function() {
187+
sd_configure_proj(shared_library = proj_dll_name())
188+
}
189+
190+
configure_proj_prefix <- function(prefix) {
191+
if (!dir.exists(prefix)) {
192+
stop(sprintf("Can't configure PROJ from prefix '%s': does not exist", prefix))
193+
}
194+
195+
sd_configure_proj(
196+
shared_library = file.path(prefix, "lib", proj_dll_name()),
197+
database_path = file.path(prefix, "share", "proj", "proj.db"),
198+
search_path = file.path(prefix, "share", "proj")
199+
)
200+
}
201+
202+
proj_dll_name <- function() {
203+
switch(tolower(Sys.info()[["sysname"]]),
204+
windows = "proj.dll",
205+
darwin = "libproj.dylib",
206+
linux = "libproj.so",
207+
stop(sprintf("Can't determine system PROJ shared library name for OS: %s", Sys.info()[["sysname"]]))
208+
)
209+
}

r/sedonadb/man/sd_configure_proj.Rd

Lines changed: 44 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

r/sedonadb/src/init.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ SEXP handle_result(SEXP res_) {
5151
return (SEXP)res;
5252
}
5353

54+
SEXP savvy_configure_proj_shared__impl(SEXP c_arg__shared_library_path,
55+
SEXP c_arg__database_path,
56+
SEXP c_arg__search_path) {
57+
SEXP res = savvy_configure_proj_shared__ffi(
58+
c_arg__shared_library_path, c_arg__database_path, c_arg__search_path);
59+
return handle_result(res);
60+
}
61+
5462
SEXP savvy_init_r_runtime__impl(DllInfo *c_arg___dll_info) {
5563
SEXP res = savvy_init_r_runtime__ffi(c_arg___dll_info);
5664
return handle_result(res);
@@ -156,6 +164,8 @@ SEXP savvy_InternalDataFrame_to_view__impl(SEXP self__, SEXP c_arg__ctx,
156164
}
157165

158166
static const R_CallMethodDef CallEntries[] = {
167+
{"savvy_configure_proj_shared__impl",
168+
(DL_FUNC)&savvy_configure_proj_shared__impl, 3},
159169
{"savvy_init_r_runtime_interrupts__impl",
160170
(DL_FUNC)&savvy_init_r_runtime_interrupts__impl, 2},
161171
{"savvy_sedonadb_adbc_init_func__impl",

r/sedonadb/src/rust/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ sedona = { path = "../../../../rust/sedona" }
3434
sedona-adbc = { path = "../../../../rust/sedona-adbc" }
3535
sedona-expr = { path = "../../../../rust/sedona-expr" }
3636
sedona-geoparquet = { path = "../../../../rust/sedona-geoparquet" }
37+
sedona-proj = { path = "../../../../c/sedona-proj", default-features = false }
3738
sedona-schema = { path = "../../../../rust/sedona-schema" }
3839
thiserror = { workspace = true }
3940
tokio = { workspace = true }

r/sedonadb/src/rust/api.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18+
SEXP savvy_configure_proj_shared__ffi(SEXP c_arg__shared_library_path,
19+
SEXP c_arg__database_path,
20+
SEXP c_arg__search_path);
1821
SEXP savvy_init_r_runtime__ffi(DllInfo *c_arg___dll_info);
1922
SEXP savvy_init_r_runtime_interrupts__ffi(SEXP c_arg__interrupts_call,
2023
SEXP c_arg__pkg_env);

r/sedonadb/src/rust/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use savvy::savvy;
2222

2323
use savvy_ffi::R_NilValue;
2424
use sedona_adbc::AdbcSedonadbDriverInit;
25+
use sedona_proj::register::{configure_global_proj_engine, ProjCrsEngineBuilder};
2526

2627
mod context;
2728
mod dataframe;
@@ -40,3 +41,27 @@ fn sedonadb_adbc_init_func() -> savvy::Result<savvy::Sexp> {
4041
)))
4142
}
4243
}
44+
45+
#[savvy]
46+
fn configure_proj_shared(
47+
shared_library_path: Option<&str>,
48+
database_path: Option<&str>,
49+
search_path: Option<&str>,
50+
) -> savvy::Result<()> {
51+
let mut builder = ProjCrsEngineBuilder::default();
52+
53+
if let Some(shared_library_path) = shared_library_path {
54+
builder = builder.with_shared_library(shared_library_path.into());
55+
}
56+
57+
if let Some(database_path) = database_path {
58+
builder = builder.with_database_path(database_path.into());
59+
}
60+
61+
if let Some(search_path) = search_path {
62+
builder = builder.with_search_paths(vec![search_path.into()]);
63+
}
64+
65+
configure_global_proj_engine(builder)?;
66+
Ok(())
67+
}

r/sedonadb/tests/testthat/test-context.R

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,20 @@ test_that("views can be created and dropped", {
3939
expect_error(sd_sql("SELECT * FROM foofy"), "table '(.*?)' not found")
4040
expect_error(sd_view("foofy"), "No table named 'foofy'")
4141
})
42+
43+
test_that("configure_proj() errors for invalid inputs", {
44+
expect_error(
45+
sd_configure_proj("not a preset"),
46+
"Unknown preset"
47+
)
48+
49+
expect_error(
50+
sd_configure_proj(database_path = "file that does not exist"),
51+
"Invalid database path"
52+
)
53+
54+
expect_error(
55+
sd_configure_proj(search_path = "dir that does not exist"),
56+
"Invalid search path"
57+
)
58+
})

0 commit comments

Comments
 (0)