diff --git a/Cargo.toml b/Cargo.toml index fd577f66..ff94ee86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,8 +31,8 @@ once_cell = "1.7.2" # polyfills a nightly feature semver = "1.0.14" # pgx core details -pgx = { version = "0.6.0-alpha.1" } -pgx-pg-config = { version = "0.6.0-alpha.1" } +pgx = { version = "=0.6.0-alpha.1" } +pgx-pg-config = { version = "=0.6.0-alpha.1" } # language handler support libloading = "0.7.2" @@ -58,7 +58,7 @@ proc-macro2 = "1" geiger = { version = "0.4", optional = true } # unsafe detection [dev-dependencies] -pgx-tests = { version = "0.6.0-alpha.1" } +pgx-tests = { version = "=0.6.0-alpha.1" } tempdir = "0.3.7" once_cell = "1.7.2" toml = "0.5.8" diff --git a/src/plrust.rs b/src/plrust.rs index ab78db1b..4a49cc3c 100644 --- a/src/plrust.rs +++ b/src/plrust.rs @@ -105,8 +105,10 @@ pub(crate) fn compile_function(fn_oid: pg_sys::Oid) -> eyre::Result { let generated = unsafe { UserCrate::try_from_fn_oid(db_oid, fn_oid)? }; let provisioned = generated.provision(&work_dir)?; + // We want to introduce validation here. let crate_dir = provisioned.crate_dir().to_path_buf(); - let (built, output) = provisioned.build(pg_config, target_dir.as_path())?; + let (validated, _output) = provisioned.validate(pg_config, target_dir.as_path())?; + let (built, output) = validated.build(target_dir.as_path())?; let shared_object = built.shared_object(); // store the shared object in our table diff --git a/src/tests.rs b/src/tests.rs index 73bab56f..da2656c7 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -504,9 +504,7 @@ mod tests { #[pg_test] #[search_path(@extschema@)] #[should_panic] - #[ignore] fn plrust_block_unsafe_plutonium() { - // TODO: PL/Rust should defeat the latest in cutting-edge `unsafe` obfuscation tech let definition = r#" CREATE FUNCTION super_safe() RETURNS text AS @@ -754,6 +752,71 @@ mod tests { )); assert!(our_id.is_none()) } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic(expected = "error: declaration of a `no_mangle` static")] + fn plrust_block_unsafe_no_mangle() { + let definition = r#" + CREATE OR REPLACE FUNCTION no_mangle() RETURNS BIGINT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + #[no_mangle] + #[link_section = ".init_array"] + pub static INITIALIZE: &[u8; 136] = &GOGO; + + #[no_mangle] + #[link_section = ".text"] + pub static GOGO: [u8; 136] = [ + 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 99, 104, 111, 46, 114, 105, 1, 72, 49, 4, + 36, 72, 137, 231, 106, 1, 254, 12, 36, 72, 184, 99, 102, 105, 108, 101, 49, 50, 51, 80, 72, + 184, 114, 47, 116, 109, 112, 47, 112, 111, 80, 72, 184, 111, 117, 99, 104, 32, 47, 118, 97, + 80, 72, 184, 115, 114, 47, 98, 105, 110, 47, 116, 80, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, + 72, 184, 114, 105, 1, 44, 98, 1, 46, 116, 72, 49, 4, 36, 49, 246, 86, 106, 14, 94, 72, 1, + 230, 86, 106, 19, 94, 72, 1, 230, 86, 106, 24, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, + 106, 59, 88, 15, 5, + ]; + + Some(1) + $$; + "#; + Spi::run(definition); + let result = Spi::get_one::("SELECT no_mangle();\n"); + assert_eq!(Some(1), result); + } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic(expected = "error: declaration of a static with `link_section`")] + #[ignore] // TODO: raise MSRV + fn plrust_block_unsafe_link_section() { + let definition = r#" + CREATE OR REPLACE FUNCTION link_section() RETURNS BIGINT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + #[link_section = ".init_array"] + pub static INITIALIZE: &[u8; 136] = &GOGO; + + #[link_section = ".text"] + pub static GOGO: [u8; 136] = [ + 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 99, 104, 111, 46, 114, 105, 1, 72, 49, 4, + 36, 72, 137, 231, 106, 1, 254, 12, 36, 72, 184, 99, 102, 105, 108, 101, 49, 50, 51, 80, 72, + 184, 114, 47, 116, 109, 112, 47, 112, 111, 80, 72, 184, 111, 117, 99, 104, 32, 47, 118, 97, + 80, 72, 184, 115, 114, 47, 98, 105, 110, 47, 116, 80, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, + 72, 184, 114, 105, 1, 44, 98, 1, 46, 116, 72, 49, 4, 36, 49, 246, 86, 106, 14, 94, 72, 1, + 230, 86, 106, 19, 94, 72, 1, 230, 86, 106, 24, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, + 106, 59, 88, 15, 5, + ]; + + Some(1) + $$; + "#; + Spi::run(definition); + let result = Spi::get_one::("SELECT link_section();\n"); + assert_eq!(Some(1), result); + } } #[cfg(any(test, feature = "pg_test"))] diff --git a/src/user_crate/crate_variant.rs b/src/user_crate/crate_variant.rs index ace8f633..c807088d 100644 --- a/src/user_crate/crate_variant.rs +++ b/src/user_crate/crate_variant.rs @@ -5,6 +5,7 @@ use proc_macro2::{Ident, Span}; use quote::quote; #[must_use] +#[derive(Clone)] pub(crate) enum CrateVariant { Function { arguments: Vec, diff --git a/src/user_crate/mod.rs b/src/user_crate/mod.rs index 4ac71c6b..cbeb968e 100644 --- a/src/user_crate/mod.rs +++ b/src/user_crate/mod.rs @@ -1,15 +1,29 @@ +/*! +How to actually build and load a PL/Rust function +*/ + +/* +Consider opening the documentation like so: +```shell +cargo doc --no-deps --document-private-items --open +``` +*/ mod crate_variant; mod state_built; mod state_generated; mod state_loaded; mod state_provisioned; +mod state_validated; mod target; +// TODO: These past-tense names are confusing to reason about +// Consider rewriting them to present tense? use crate_variant::CrateVariant; pub(crate) use state_built::StateBuilt; pub(crate) use state_generated::StateGenerated; pub(crate) use state_loaded::StateLoaded; pub(crate) use state_provisioned::StateProvisioned; +pub(crate) use state_validated::StateValidated; use crate::PlRustError; #[cfg(feature = "verify")] @@ -23,8 +37,34 @@ use std::{ process::Output, }; +/** +Finite state machine with "typestate" generic + +This forces `UserCrate

` to follow the linear path: +```rust +StateGenerated::try_from_$(inputs)_* + -> StateGenerated + -> StateProvisioned + -> StateValidated + -> StateBuilt + -> StateLoaded +``` +Rust's ownership types allow guaranteeing one-way consumption. +*/ pub(crate) struct UserCrate(P); +/** +Stages of PL/Rust compilation + +Each CrateState implementation has some set of fn including equivalents to +```rust +fn new(args: A) -> Self; +fn next(self, args: N) -> Self::NextCrateState; +``` + +These are currently not part of CrateState as they are type-specific and +premature abstraction would be unwise. +*/ pub(crate) trait CrateState {} impl UserCrate { @@ -51,9 +91,12 @@ impl UserCrate { pub unsafe fn try_from_fn_oid(db_oid: pg_sys::Oid, fn_oid: pg_sys::Oid) -> eyre::Result { unsafe { StateGenerated::try_from_fn_oid(db_oid, fn_oid).map(Self) } } + /// Two functions exist internally, a `safe_lib_rs` and `unsafe_lib_rs`. + /// At first, it only has access to `StateGenerated::safe_lib_rs` due to the FSM. #[tracing::instrument(level = "debug", skip_all)] pub fn lib_rs(&self) -> eyre::Result { - self.0.lib_rs() + let (_, lib_rs) = self.0.safe_lib_rs()?; + Ok(lib_rs) } #[tracing::instrument(level = "debug", skip_all)] pub fn cargo_toml(&self) -> eyre::Result { @@ -76,13 +119,13 @@ impl UserCrate { crate_dir = %self.0.crate_dir().display(), target_dir = tracing::field::display(target_dir.display()), ))] - pub fn build( + pub fn validate( self, pg_config: PathBuf, target_dir: &Path, - ) -> eyre::Result<(UserCrate, Output)> { + ) -> eyre::Result<(UserCrate, Output)> { self.0 - .build(pg_config, target_dir) + .validate(pg_config, target_dir) .map(|(state, output)| (UserCrate(state), output)) } @@ -91,6 +134,23 @@ impl UserCrate { } } +impl UserCrate { + #[tracing::instrument( + level = "debug", + skip_all, + fields( + db_oid = %self.0.db_oid(), + fn_oid = %self.0.fn_oid(), + crate_dir = %self.0.crate_dir().display(), + target_dir = tracing::field::display(target_dir.display()), + ))] + pub fn build(self, target_dir: &Path) -> eyre::Result<(UserCrate, Output)> { + self.0 + .build(target_dir) + .map(|(state, output)| (UserCrate(state), output)) + } +} + impl UserCrate { #[tracing::instrument(level = "debug")] pub(crate) fn built( @@ -380,9 +440,8 @@ mod tests { let generated_lib_rs = generated.lib_rs()?; let fixture_lib_rs = parse_quote! { - #![deny(unsafe_op_in_unsafe_fn)] + #![forbid(unsafe_code)] use pgx::prelude::*; - #[pg_extern] fn #symbol_ident(arg0: &str) -> Option { Some(arg0.to_string()) } @@ -408,7 +467,7 @@ mod tests { crate-type = ["cdylib"] [dependencies] - pgx = { version = "0.6.0-alpha.1", features = ["plrust"] } + pgx = { version = "=0.6.0-alpha.1", features = ["plrust"] } pallocator = { version = "0.1.0", git = "https://github.com/tcdi/postgrestd", branch = "1.61" } /* User deps added here */ @@ -427,7 +486,9 @@ mod tests { let provisioned = generated.provision(&target_dir)?; - let (built, _output) = provisioned.build(pg_config, &target_dir)?; + let (validated, _output) = provisioned.validate(pg_config, &target_dir)?; + + let (built, _output) = validated.build(&target_dir)?; let _shared_object = built.shared_object(); diff --git a/src/user_crate/state_built.rs b/src/user_crate/state_built.rs index 4d61e344..ca318348 100644 --- a/src/user_crate/state_built.rs +++ b/src/user_crate/state_built.rs @@ -12,6 +12,7 @@ pub(crate) struct StateBuilt { impl CrateState for StateBuilt {} +/// Available and ready-to-reload PL/Rust function impl StateBuilt { #[tracing::instrument(level = "debug", skip_all)] pub(crate) fn new( diff --git a/src/user_crate/state_generated.rs b/src/user_crate/state_generated.rs index ac8519f2..22265d9a 100644 --- a/src/user_crate/state_generated.rs +++ b/src/user_crate/state_generated.rs @@ -10,6 +10,7 @@ use std::path::Path; impl CrateState for StateGenerated {} +/// Entry point for new crates via FSM #[must_use] pub(crate) struct StateGenerated { pg_proc_xmin: pg_sys::TransactionId, @@ -85,41 +86,46 @@ impl StateGenerated { }) } pub(crate) fn crate_name(&self) -> String { - crate::plrust::crate_name(self.db_oid, self.fn_oid) + let mut _crate_name = crate::plrust::crate_name(self.db_oid, self.fn_oid); + #[cfg(any( + all(target_os = "macos", target_arch = "x86_64"), + feature = "force_enable_x86_64_darwin_generations" + ))] + { + let next = crate::generation::next_generation(&_crate_name, true).unwrap_or_default(); + _crate_name.push_str(&format!("_{}", next)); + } + _crate_name } + /// Generates the initial, "pure Rust" wrapper for the PL/Rust function, + /// allowing it to be used for typechecking. #[tracing::instrument(level = "debug", skip_all, fields(db_oid = %self.db_oid, fn_oid = %self.fn_oid))] - pub(crate) fn lib_rs(&self) -> eyre::Result { + pub(crate) fn safe_lib_rs(&self) -> eyre::Result<(syn::ItemFn, syn::File)> { + // Hello from the futurepast! + // The only situation in which you should be removing this `#![forbid(unsafe_code)]` + // from the skeleton code is if you are moving the forbid command somewhere else + // or reconfiguring PL/Rust to also allow it to be run in a fully "untrusted" mode. + // This is what does all of the code checking not only for `unsafe {}` but also + // "unsafe attributes" which are considered unsafe but don't have the `unsafe` token. let mut skeleton: syn::File = syn::parse_quote!( - #![deny(unsafe_op_in_unsafe_fn)] + #![forbid(unsafe_code)] use pgx::prelude::*; ); let crate_name = self.crate_name(); - #[cfg(any( - all(target_os = "macos", target_arch = "x86_64"), - feature = "force_enable_x86_64_darwin_generations" - ))] - let crate_name = { - let mut crate_name = crate_name; - let next = crate::generation::next_generation(&crate_name, true).unwrap_or_default(); - - crate_name.push_str(&format!("_{}", next)); - crate_name - }; let symbol_ident = proc_macro2::Ident::new(&crate_name, proc_macro2::Span::call_site()); - tracing::trace!(symbol_name = %crate_name, "Generating `lib.rs`"); + tracing::trace!(symbol_name = %crate_name, "Generating `lib.rs` for validation step"); let user_code = &self.user_code; - let user_function = match &self.variant { + let user_fn = match &self.variant { CrateVariant::Function { ref arguments, ref return_type, .. } => { let user_fn: syn::ItemFn = syn::parse2(quote! { - #[pg_extern] fn #symbol_ident( #( #arguments ),* ) -> #return_type @@ -130,10 +136,9 @@ impl StateGenerated { } CrateVariant::Trigger => { let user_fn: syn::ItemFn = syn::parse2(quote! { - #[pg_trigger] fn #symbol_ident( trigger: &::pgx::PgTrigger, - ) -> core::result::Result< + ) -> ::core::result::Result< ::pgx::heap_tuple::PgHeapTuple<'_, impl ::pgx::WhoAllocated<::pgx::pg_sys::HeapTupleData>>, Box, > #user_code @@ -143,8 +148,8 @@ impl StateGenerated { } }; - skeleton.items.push(user_function.into()); - Ok(skeleton) + skeleton.items.push(user_fn.clone().into()); + Ok((user_fn, skeleton)) } #[tracing::instrument(level = "debug", skip_all, fields(db_oid = %self.db_oid, fn_oid = %self.fn_oid))] @@ -153,18 +158,6 @@ impl StateGenerated { let version_feature = format!("pgx/pg{major_version}"); let crate_name = self.crate_name(); - #[cfg(any( - all(target_os = "macos", target_arch = "x86_64"), - feature = "force_enable_x86_64_darwin_generations" - ))] - let crate_name = { - let mut crate_name = crate_name; - let next = crate::generation::next_generation(&crate_name, true).unwrap_or_default(); - - crate_name.push_str(&format!("_{}", next)); - crate_name - }; - tracing::trace!( crate_name = %crate_name, user_dependencies = ?self.user_dependencies.keys().cloned().collect::>(), @@ -184,7 +177,7 @@ impl StateGenerated { crate-type = ["cdylib"] [dependencies] - pgx = { version = "0.6.0-alpha.1", features = ["plrust"] } + pgx = { version = "=0.6.0-alpha.1", features = ["plrust"] } pallocator = { version = "0.1.0", git = "https://github.com/tcdi/postgrestd", branch = "1.61" } /* User deps added here */ @@ -264,7 +257,7 @@ impl StateGenerated { "Could not create crate directory in configured `plrust.work_dir` location", )?; - let lib_rs = self.lib_rs()?; + let (user_fn, lib_rs) = self.safe_lib_rs()?; let lib_rs_path = src_dir.join("lib.rs"); std::fs::write(&lib_rs_path, &prettyplease::unparse(&lib_rs)) .wrap_err("Writing generated `lib.rs`")?; @@ -283,6 +276,8 @@ impl StateGenerated { self.fn_oid, crate_name, crate_dir, + user_fn, + self.variant.clone(), )) } @@ -346,11 +341,10 @@ mod tests { }; let symbol_ident = proc_macro2::Ident::new(&crate_name, proc_macro2::Span::call_site()); - let generated_lib_rs = generated.lib_rs()?; + let (_, generated_lib_rs) = generated.safe_lib_rs()?; let fixture_lib_rs = parse_quote! { - #![deny(unsafe_op_in_unsafe_fn)] + #![forbid(unsafe_code)] use pgx::prelude::*; - #[pg_extern] fn #symbol_ident(arg0: &str) -> Option { Some(arg0.to_string()) } @@ -411,11 +405,10 @@ mod tests { }; let symbol_ident = proc_macro2::Ident::new(&crate_name, proc_macro2::Span::call_site()); - let generated_lib_rs = generated.lib_rs()?; + let (_, generated_lib_rs) = generated.safe_lib_rs()?; let fixture_lib_rs = parse_quote! { - #![deny(unsafe_op_in_unsafe_fn)] + #![forbid(unsafe_code)] use pgx::prelude::*; - #[pg_extern] fn #symbol_ident(val: Option) -> Option { val.map(|v| v as i64) } @@ -476,11 +469,10 @@ mod tests { }; let symbol_ident = proc_macro2::Ident::new(&crate_name, proc_macro2::Span::call_site()); - let generated_lib_rs = generated.lib_rs()?; + let (_, generated_lib_rs) = generated.safe_lib_rs()?; let fixture_lib_rs = parse_quote! { - #![deny(unsafe_op_in_unsafe_fn)] + #![forbid(unsafe_code)] use pgx::prelude::*; - #[pg_extern] fn #symbol_ident(val: &str) -> Option<::pgx::iter::SetOfIterator>> { Some(std::iter::repeat(val).take(5)) } @@ -532,14 +524,13 @@ mod tests { }; let symbol_ident = proc_macro2::Ident::new(&crate_name, proc_macro2::Span::call_site()); - let generated_lib_rs = generated.lib_rs()?; + let (_, generated_lib_rs) = generated.safe_lib_rs()?; let fixture_lib_rs = parse_quote! { - #![deny(unsafe_op_in_unsafe_fn)] + #![forbid(unsafe_code)] use pgx::prelude::*; - #[pg_trigger] fn #symbol_ident( trigger: &::pgx::PgTrigger, - ) -> core::result::Result< + ) -> ::core::result::Result< ::pgx::heap_tuple::PgHeapTuple<'_, impl ::pgx::WhoAllocated<::pgx::pg_sys::HeapTupleData>>, Box, > { diff --git a/src/user_crate/state_loaded.rs b/src/user_crate/state_loaded.rs index bfde1100..6bb26f18 100644 --- a/src/user_crate/state_loaded.rs +++ b/src/user_crate/state_loaded.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; impl CrateState for StateLoaded {} +/// Available and ready-to-reload PL/Rust function #[must_use] pub(crate) struct StateLoaded { pg_proc_xmin: pg_sys::TransactionId, diff --git a/src/user_crate/state_provisioned.rs b/src/user_crate/state_provisioned.rs index 39b7b880..935fac8c 100644 --- a/src/user_crate/state_provisioned.rs +++ b/src/user_crate/state_provisioned.rs @@ -1,7 +1,22 @@ -use crate::{ - user_crate::{target, CrateState, StateBuilt}, - PlRustError, -}; +/*! +Provisioned and ready for validation steps + +To detect unsafe code in PL/Rust while still using PGX requires some circumlocution. +PGX creates `#[no_mangle] unsafe extern "C" fn` wrappers that allow Postgres to call Rust, +as PostgreSQL will dynamically load what it thinks is a C library and call C ABI wrapper fn +that themselves handle the Postgres fn call ABI for the programmer and then, finally, +call into the programmer's Rust ABI fn! +This blocks simply using rustc's `unsafe` detection as pgx-macros generated code is unsafe. + +The circumlocution is brutal, simple, and effective: +pgx-macros wraps actual Rust which can be safe if it contains no unsafe code! +Such code is powerless (it really, truly, will not run, and may not even build) +but it should still typecheck. Writing an empty shell function first +allows using the linting power of rustc on it as a validation step. +Then the function can be rewritten with annotations from pgx-macros injected. +*/ + +use crate::user_crate::{target, CrateState, CrateVariant, PlRustError, StateValidated}; use color_eyre::{Section, SectionExt}; use eyre::{eyre, WrapErr}; use pgx::pg_sys; @@ -10,6 +25,7 @@ use std::{ process::{Command, Output}, }; +/// Provisioned and ready to validate #[must_use] pub(crate) struct StateProvisioned { pg_proc_xmin: pg_sys::TransactionId, @@ -17,10 +33,14 @@ pub(crate) struct StateProvisioned { fn_oid: pg_sys::Oid, crate_name: String, crate_dir: PathBuf, + user_fn: syn::ItemFn, + variant: CrateVariant, } impl CrateState for StateProvisioned {} +/// Note that this is the step which actually executes validation +/// because of past tense naming. impl StateProvisioned { #[tracing::instrument(level = "debug", skip_all, fields(db_oid = %db_oid, fn_oid = %fn_oid, crate_name = %crate_name, crate_dir = %crate_dir.display()))] pub(crate) fn new( @@ -29,6 +49,8 @@ impl StateProvisioned { fn_oid: pg_sys::Oid, crate_name: String, crate_dir: PathBuf, + user_fn: syn::ItemFn, + variant: CrateVariant, ) -> Self { Self { pg_proc_xmin, @@ -36,8 +58,41 @@ impl StateProvisioned { fn_oid, crate_name, crate_dir, + user_fn, + variant, } } + + // TODO: Maybe should be in next state? Prevent access until validated? + // This only would be very useful if it was also module-abstracted. + #[tracing::instrument(level = "debug", skip_all, fields(db_oid = %self.db_oid, fn_oid = %self.fn_oid))] + pub(crate) fn unsafe_lib_rs(&self) -> eyre::Result { + let mut skeleton: syn::File = syn::parse_quote!( + #![deny(unsafe_op_in_unsafe_fn)] + use pgx::prelude::*; + ); + + let crate_name = &self.crate_name; + tracing::trace!(symbol_name = %crate_name, "Generating `lib.rs` for build step"); + + let mut user_fn = self.user_fn.clone(); + match &self.variant { + CrateVariant::Function { .. } => { + user_fn.attrs.push(syn::parse_quote! { + #[pg_extern] + }); + } + CrateVariant::Trigger => { + user_fn.attrs.push(syn::parse_quote! { + #[pg_trigger] + }); + } + }; + + skeleton.items.push(user_fn.into()); + Ok(skeleton) + } + #[tracing::instrument( level = "debug", skip_all, @@ -47,30 +102,34 @@ impl StateProvisioned { crate_dir = %self.crate_dir.display(), target_dir = tracing::field::display(target_dir.display()), ))] - pub(crate) fn build( + pub(crate) fn validate( self, pg_config: PathBuf, target_dir: &Path, - ) -> eyre::Result<(StateBuilt, Output)> { + ) -> eyre::Result<(StateValidated, Output)> { let mut command = Command::new("cargo"); let target = target::tuple()?; let target_str = ⌖ command.current_dir(&self.crate_dir); - command.arg("rustc"); - command.arg("--release"); + command.arg("check"); command.arg("--target"); command.arg(target_str); - command.env("PGX_PG_CONFIG_PATH", pg_config); + command.env("PGX_PG_CONFIG_PATH", &pg_config); command.env("CARGO_TARGET_DIR", &target_dir); command.env("RUSTFLAGS", "-Clink-args=-Wl,-undefined,dynamic_lookup"); let output = command.output().wrap_err("`cargo` execution failure")?; + // TODO: Maybe this should instead be an explicit re-provisioning step? if output.status.success() { - use std::env::consts::DLL_SUFFIX; + let crate_name = self.crate_name.clone(); - let crate_name = self.crate_name; + // rebuild code: + let lib_rs = self.unsafe_lib_rs()?; + let lib_rs_path = self.crate_dir.join("src/lib.rs"); + std::fs::write(&lib_rs_path, &prettyplease::unparse(&lib_rs)) + .wrap_err("Writing generated `lib.rs`")?; #[cfg(any( all(target_os = "macos", target_arch = "x86_64"), @@ -86,18 +145,14 @@ impl StateProvisioned { crate_name }; - let built_shared_object_name = &format!("lib{crate_name}{DLL_SUFFIX}"); - let built_shared_object = target_dir - .join(target_str) - .join("release") - .join(&built_shared_object_name); - Ok(( - StateBuilt::new( + StateValidated::new( self.pg_proc_xmin, self.db_oid, self.fn_oid, - built_shared_object, + crate_name, + self.crate_dir, + pg_config, ), output, )) diff --git a/src/user_crate/state_validated.rs b/src/user_crate/state_validated.rs new file mode 100644 index 00000000..d0d3942a --- /dev/null +++ b/src/user_crate/state_validated.rs @@ -0,0 +1,133 @@ +use crate::{ + user_crate::{target, CrateState, StateBuilt}, + PlRustError, +}; +use color_eyre::{Section, SectionExt}; +use eyre::{eyre, WrapErr}; +use pgx::pg_sys; +use std::{ + path::{Path, PathBuf}, + process::{Command, Output}, +}; + +/// Validated and ready to build +#[must_use] +pub(crate) struct StateValidated { + pg_proc_xmin: pg_sys::TransactionId, + db_oid: pg_sys::Oid, + fn_oid: pg_sys::Oid, + crate_name: String, + crate_dir: PathBuf, + pg_config: PathBuf, +} + +impl CrateState for StateValidated {} + +impl StateValidated { + #[tracing::instrument(level = "debug", skip_all, fields(db_oid = %db_oid, fn_oid = %fn_oid, crate_name = %crate_name, crate_dir = %crate_dir.display()))] + pub(crate) fn new( + pg_proc_xmin: pg_sys::TransactionId, + db_oid: pg_sys::Oid, + fn_oid: pg_sys::Oid, + crate_name: String, + crate_dir: PathBuf, + pg_config: PathBuf, + ) -> Self { + Self { + pg_proc_xmin, + db_oid, + fn_oid, + crate_name, + crate_dir, + pg_config, + } + } + + #[tracing::instrument( + level = "debug", + skip_all, + fields( + db_oid = %self.db_oid, + fn_oid = %self.fn_oid, + crate_dir = %self.crate_dir.display(), + target_dir = tracing::field::display(target_dir.display()), + ))] + pub(crate) fn build(self, target_dir: &Path) -> eyre::Result<(StateBuilt, Output)> { + let mut command = Command::new("cargo"); + let target = target::tuple()?; + let target_str = ⌖ + + command.current_dir(&self.crate_dir); + command.arg("rustc"); + command.arg("--release"); + command.arg("--target"); + command.arg(target_str); + command.env("PGX_PG_CONFIG_PATH", self.pg_config); + command.env("CARGO_TARGET_DIR", &target_dir); + command.env("RUSTFLAGS", "-Clink-args=-Wl,-undefined,dynamic_lookup"); + + let output = command.output().wrap_err("`cargo` execution failure")?; + + if output.status.success() { + use std::env::consts::DLL_SUFFIX; + + let crate_name = self.crate_name; + + #[cfg(any( + all(target_os = "macos", target_arch = "x86_64"), + feature = "force_enable_x86_64_darwin_generations" + ))] + let crate_name = { + let mut crate_name = crate_name; + let next = crate::generation::next_generation(&crate_name, true) + .map(|gen_num| gen_num) + .unwrap_or_default(); + + crate_name.push_str(&format!("_{}", next)); + crate_name + }; + + let built_shared_object_name = &format!("lib{crate_name}{DLL_SUFFIX}"); + let built_shared_object = target_dir + .join(target_str) + .join("release") + .join(&built_shared_object_name); + + Ok(( + StateBuilt::new( + self.pg_proc_xmin, + self.db_oid, + self.fn_oid, + built_shared_object, + ), + output, + )) + } else { + let stdout = + String::from_utf8(output.stdout).wrap_err("`cargo`'s stdout was not UTF-8")?; + let stderr = + String::from_utf8(output.stderr).wrap_err("`cargo`'s stderr was not UTF-8")?; + + Err(eyre!(PlRustError::CargoBuildFail) + .section(stdout.header("`cargo build` stdout:")) + .section(stderr.header("`cargo build` stderr:")) + .with_section(|| { + std::fs::read_to_string(&self.crate_dir.join("src").join("lib.rs")) + .wrap_err("Writing generated `lib.rs`") + .expect("Reading generated `lib.rs` to output during error") + .header("Source Code:") + }))? + } + } + + pub(crate) fn fn_oid(&self) -> pg_sys::Oid { + self.fn_oid + } + + pub(crate) fn db_oid(&self) -> pg_sys::Oid { + self.db_oid + } + pub(crate) fn crate_dir(&self) -> &Path { + &self.crate_dir + } +} diff --git a/src/user_crate/target.rs b/src/user_crate/target.rs index b1b7975b..d0d8d440 100644 --- a/src/user_crate/target.rs +++ b/src/user_crate/target.rs @@ -2,7 +2,7 @@ This prevents using the "fallback" logic of Cargo leaving builds in an unlabeled directory. This is a precaution as PL/Rust is a cross-compiler. so a normal build-and-test cycle may create artifacts for multiple targets. -!*/ +*/ use std::env; use std::ffi::OsString;