use bindgen::callbacks::{DeriveTrait, EnumVariantValue, ImplementsTrait, MacroParsingBehavior};
use bindgen::NonCopyUnionStyle;
use eyre::{eyre, WrapErr};
use pgrx_pg_config::{
is_supported_major_version, prefix_path, PgConfig, PgConfigSelector, Pgrx, SUPPORTED_VERSIONS,
};
use quote::{quote, ToTokens};
use std::cell::RefCell;
use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fs;
use std::path::{self, Path, PathBuf}; use std::process::{Command, Output};
use std::rc::Rc;
use std::sync::OnceLock;
use syn::{ForeignItem, Item, ItemConst};
const BLOCKLISTED_TYPES: [&str; 3] = ["Datum", "NullableDatum", "Oid"];
mod build {
pub(super) mod clang;
pub(super) mod sym_blocklist;
}
#[derive(Debug)]
struct BindingOverride {
ignore_macros: HashSet<&'static str>,
enum_names: InnerMut<EnumMap>,
}
type InnerMut<T> = Rc<RefCell<T>>;
type EnumMap = BTreeMap<String, Vec<(String, EnumVariantValue)>>;
impl BindingOverride {
fn new_from(enum_names: InnerMut<EnumMap>) -> Self {
BindingOverride {
ignore_macros: HashSet::from_iter([
"FP_INFINITE",
"FP_NAN",
"FP_NORMAL",
"FP_SUBNORMAL",
"FP_ZERO",
"IPPORT_RESERVED",
"M_E",
"M_LOG2E",
"M_LOG10E",
"M_LN2",
"M_LN10",
"M_PI",
"M_PI_2",
"M_PI_4",
"M_1_PI",
"M_2_PI",
"M_SQRT2",
"M_SQRT1_2",
"M_2_SQRTPI",
]),
enum_names,
}
}
}
impl bindgen::callbacks::ParseCallbacks for BindingOverride {
fn will_parse_macro(&self, name: &str) -> MacroParsingBehavior {
if self.ignore_macros.contains(name) {
bindgen::callbacks::MacroParsingBehavior::Ignore
} else {
bindgen::callbacks::MacroParsingBehavior::Default
}
}
fn blocklisted_type_implements_trait(
&self,
name: &str,
derive_trait: DeriveTrait,
) -> Option<ImplementsTrait> {
if !BLOCKLISTED_TYPES.contains(&name) {
return None;
}
let implements_trait = match derive_trait {
DeriveTrait::Copy => ImplementsTrait::Yes,
DeriveTrait::Debug => ImplementsTrait::Yes,
_ => ImplementsTrait::No,
};
Some(implements_trait)
}
fn int_macro(&self, _name: &str, _value: i64) -> Option<bindgen::callbacks::IntKind> {
None
}
fn func_macro(&self, _name: &str, _value: &[&[u8]]) {}
fn enum_variant_behavior(
&self,
enum_name: Option<&str>,
variant_name: &str,
variant_value: bindgen::callbacks::EnumVariantValue,
) -> Option<bindgen::callbacks::EnumVariantCustomBehavior> {
enum_name.inspect(|name| match name.strip_prefix("enum").unwrap_or(name).trim() {
"NodeTag" => return,
name if name.contains("unnamed at") => return,
_ if variant_name.contains("OID") => return,
name => self
.enum_names
.borrow_mut()
.entry(name.to_string())
.or_insert(Vec::new())
.push((variant_name.to_string(), variant_value)),
});
None
}
fn field_visibility(
&self,
_info: bindgen::callbacks::FieldInfo<'_>,
) -> Option<bindgen::FieldVisibilityKind> {
None
}
}
fn main() -> eyre::Result<()> {
if env_tracked("DOCS_RS").as_deref() == Some("1") {
return Ok(());
}
if env_tracked("PGRX_BUILD_VERBOSE").as_deref() == Some("true") {
for (k, v) in std::env::vars() {
eprintln!("{k}={v}");
}
}
let compile_cshim = env_tracked("CARGO_FEATURE_CSHIM").as_deref() == Some("1");
let is_for_release =
env_tracked("PGRX_PG_SYS_GENERATE_BINDINGS_FOR_RELEASE").as_deref() == Some("1");
let build_paths = BuildPaths::from_env();
eprintln!("build_paths={build_paths:?}");
emit_rerun_if_changed();
let pg_configs: Vec<(u16, PgConfig)> = if is_for_release {
Pgrx::from_config()?.iter(PgConfigSelector::All)
.map(|r| r.expect("invalid pg_config"))
.map(|c| (c.major_version().expect("invalid major version"), c))
.filter_map(|t| {
if is_supported_major_version(t.0) {
Some(t)
} else {
println!(
"cargo:warning={} contains a configuration for pg{}, which pgrx does not support.",
Pgrx::config_toml()
.expect("Could not get PGRX configuration TOML")
.to_string_lossy(),
t.0
);
None
}
})
.collect()
} else {
let mut found = Vec::new();
for pgver in SUPPORTED_VERSIONS() {
if env_tracked(&format!("CARGO_FEATURE_PG{}", pgver.major)).is_some() {
found.push(pgver);
}
}
let found_ver = match &found[..] {
[ver] => ver,
[] => {
return Err(eyre!(
"Did not find `pg$VERSION` feature. `pgrx-pg-sys` requires one of {} to be set",
SUPPORTED_VERSIONS()
.iter()
.map(|pgver| format!("`pg{}`", pgver.major))
.collect::<Vec<_>>()
.join(", ")
))
}
versions => {
return Err(eyre!(
"Multiple `pg$VERSION` features found.\n`--no-default-features` may be required.\nFound: {}",
versions
.iter()
.map(|version| format!("pg{}", version.major))
.collect::<Vec<String>>()
.join(", ")
))
}
};
let found_major = found_ver.major;
if let Ok(pg_config) = PgConfig::from_env() {
let major_version = pg_config.major_version()?;
if major_version != found_major {
panic!("Feature flag `pg{found_major}` does not match version from the environment-described PgConfig (`{major_version}`)")
}
vec![(major_version, pg_config)]
} else {
let specific = Pgrx::from_config()?.get(&format!("pg{}", found_ver.major))?;
vec![(found_ver.major, specific)]
}
};
std::thread::scope(|scope| {
let threads = pg_configs
.iter()
.map(|(pg_major_ver, pg_config)| {
scope.spawn(|| {
generate_bindings(*pg_major_ver, pg_config, &build_paths, is_for_release)
})
})
.collect::<Vec<_>>();
let results = threads
.into_iter()
.map(|thread| thread.join().expect("thread panicked while generating bindings"))
.collect::<Vec<eyre::Result<_>>>();
results.into_iter().try_for_each(|r| r)
})?;
if compile_cshim {
for (_version, pg_config) in pg_configs {
build_shim(&build_paths.shim_src, &build_paths.shim_dst, &pg_config)?;
}
}
Ok(())
}
fn emit_rerun_if_changed() {
println!("cargo:rerun-if-env-changed=PGRX_PG_CONFIG_PATH");
println!("cargo:rerun-if-env-changed=PGRX_PG_CONFIG_AS_ENV");
println!("cargo:rerun-if-env-changed=LLVM_CONFIG_PATH");
println!("cargo:rerun-if-env-changed=LIBCLANG_PATH");
println!("cargo:rerun-if-env-changed=LIBCLANG_STATIC_PATH");
println!("cargo:rerun-if-env-changed=BINDGEN_EXTRA_CLANG_ARGS");
if let Some(target) = env_tracked("TARGET") {
println!("cargo:rerun-if-env-changed=BINDGEN_EXTRA_CLANG_ARGS_{target}");
println!(
"cargo:rerun-if-env-changed=BINDGEN_EXTRA_CLANG_ARGS_{}",
target.replace('-', "_"),
);
}
println!("cargo:rerun-if-env-changed=PGRX_PG_SYS_GENERATE_BINDINGS_FOR_RELEASE");
println!("cargo:rerun-if-changed=include");
println!("cargo:rerun-if-changed=cshim");
if let Ok(pgrx_config) = Pgrx::config_toml() {
println!("cargo:rerun-if-changed={}", pgrx_config.display());
}
}
fn generate_bindings(
major_version: u16,
pg_config: &PgConfig,
build_paths: &BuildPaths,
is_for_release: bool,
) -> eyre::Result<()> {
let mut include_h = build_paths.manifest_dir.clone();
include_h.push("include");
include_h.push(format!("pg{major_version}.h"));
let bindgen_output = get_bindings(major_version, pg_config, &include_h)
.wrap_err_with(|| format!("bindgen failed for pg{major_version}"))?;
let oids = extract_oids(&bindgen_output);
let rewritten_items = rewrite_items(&bindgen_output, &oids)
.wrap_err_with(|| format!("failed to rewrite items for pg{major_version}"))?;
let oids = format_builtin_oid_impl(oids);
let dest_dirs = if is_for_release {
vec![build_paths.out_dir.clone(), build_paths.src_dir.clone()]
} else {
vec![build_paths.out_dir.clone()]
};
for dest_dir in dest_dirs {
let mut bindings_file = dest_dir.clone();
bindings_file.push(&format!("pg{major_version}.rs"));
write_rs_file(
rewritten_items.clone(),
&bindings_file,
quote! {
use crate as pg_sys;
use crate::{Datum, Oid, PgNode};
},
is_for_release,
)
.wrap_err_with(|| {
format!(
"Unable to write bindings file for pg{} to `{}`",
major_version,
bindings_file.display()
)
})?;
let mut oids_file = dest_dir.clone();
oids_file.push(&format!("pg{major_version}_oids.rs"));
write_rs_file(oids.clone(), &oids_file, quote! {}, is_for_release).wrap_err_with(|| {
format!(
"Unable to write oids file for pg{} to `{}`",
major_version,
oids_file.display()
)
})?;
}
Ok(())
}
#[derive(Debug, Clone)]
struct BuildPaths {
manifest_dir: PathBuf,
out_dir: PathBuf,
src_dir: PathBuf,
shim_src: PathBuf,
shim_dst: PathBuf,
}
impl BuildPaths {
fn from_env() -> Self {
let manifest_dir = env_tracked("CARGO_MANIFEST_DIR").map(PathBuf::from).unwrap();
let out_dir = env_tracked("OUT_DIR").map(PathBuf::from).unwrap();
Self {
src_dir: manifest_dir.join("src/include"),
shim_src: manifest_dir.join("cshim"),
shim_dst: out_dir.join("cshim"),
out_dir,
manifest_dir,
}
}
}
fn write_rs_file(
code: proc_macro2::TokenStream,
file_path: &Path,
header: proc_macro2::TokenStream,
is_for_release: bool,
) -> eyre::Result<()> {
use std::io::Write;
let mut contents = header;
contents.extend(code);
let mut file = fs::File::create(file_path)?;
write!(file, "/* Automatically generated by bindgen. Do not hand-edit.")?;
if is_for_release {
write!(
file,
"\n
This code is generated for documentation purposes, so that it is
easy to reference on docs.rs. Bindings are regenerated for your
build of pgrx, and the values of your Postgres version may differ.
*/"
)
} else {
write!(file, " */")
}?;
write!(file, "{contents}")?;
rust_fmt(file_path)
}
fn rewrite_items(
file: &syn::File,
oids: &BTreeMap<syn::Ident, Box<syn::Expr>>,
) -> eyre::Result<proc_macro2::TokenStream> {
let items_vec = rewrite_oid_consts(&file.items, oids);
let mut items = apply_pg_guard(&items_vec)?;
let pgnode_impls = impl_pg_node(&items_vec)?;
items.extend(pgnode_impls);
Ok(items)
}
fn extract_oids(code: &syn::File) -> BTreeMap<syn::Ident, Box<syn::Expr>> {
let mut oids = BTreeMap::new(); for item in &code.items {
let Item::Const(ItemConst { ident, ty, expr, .. }) = item else { continue };
let name = ident.to_string();
let ty_str = ty.to_token_stream().to_string();
if ty_str == "u32" && is_builtin_oid(&name) {
oids.insert(ident.clone(), expr.clone());
}
}
oids
}
fn is_builtin_oid(name: &str) -> bool {
name.ends_with("OID") && name != "HEAP_HASOID"
|| name.ends_with("RelationId")
|| name == "TemplateDbOid"
}
fn rewrite_oid_consts(
items: &[syn::Item],
oids: &BTreeMap<syn::Ident, Box<syn::Expr>>,
) -> Vec<syn::Item> {
items
.iter()
.map(|item| match item {
Item::Const(ItemConst { ident, ty, expr, .. })
if ty.to_token_stream().to_string() == "u32" && oids.get(ident) == Some(expr) =>
{
syn::parse2(quote! { pub const #ident : Oid = Oid(#expr); }).unwrap()
}
item => item.clone(),
})
.collect()
}
fn format_builtin_oid_impl(oids: BTreeMap<syn::Ident, Box<syn::Expr>>) -> proc_macro2::TokenStream {
let enum_variants: proc_macro2::TokenStream;
let from_impl: proc_macro2::TokenStream;
(enum_variants, from_impl) = oids
.iter()
.map(|(ident, expr)| {
(quote! { #ident = #expr, }, quote! { #expr => Ok(BuiltinOid::#ident), })
})
.unzip();
quote! {
use crate::{NotBuiltinOid};
#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Debug)]
pub enum BuiltinOid {
#enum_variants
}
impl BuiltinOid {
pub const fn from_u32(uint: u32) -> Result<BuiltinOid, NotBuiltinOid> {
match uint {
0 => Err(NotBuiltinOid::Invalid),
#from_impl
_ => Err(NotBuiltinOid::Ambiguous),
}
}
}
}
}
fn impl_pg_node(items: &[syn::Item]) -> eyre::Result<proc_macro2::TokenStream> {
let mut pgnode_impls = proc_macro2::TokenStream::new();
let struct_graph: StructGraph = StructGraph::from(items);
let mut root_node_structs = Vec::new();
for descriptor in struct_graph.descriptors.iter() {
let first_field = match &descriptor.struct_.fields {
syn::Fields::Named(fields) => {
if let Some(first_field) = fields.named.first() {
first_field
} else {
continue;
}
}
syn::Fields::Unnamed(fields) => {
if let Some(first_field) = fields.unnamed.first() {
first_field
} else {
continue;
}
}
_ => continue,
};
let ty_name = if let syn::Type::Path(p) = &first_field.ty {
if let Some(last_segment) = p.path.segments.last() {
last_segment.ident.to_string()
} else {
continue;
}
} else {
continue;
};
if ty_name == "NodeTag" {
root_node_structs.push(descriptor);
}
}
let mut node_set = BTreeSet::new();
for root in root_node_structs.into_iter() {
dfs_find_nodes(root, &struct_graph, &mut node_set);
}
for node_struct in node_set.into_iter() {
let struct_name = &node_struct.struct_.ident;
pgnode_impls.extend(quote! {
impl pg_sys::seal::Sealed for #struct_name {}
impl pg_sys::PgNode for #struct_name {}
});
pgnode_impls.extend(quote! {
impl ::core::fmt::Display for #struct_name {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
self.display_node().fmt(f)
}
}
});
}
Ok(pgnode_impls)
}
fn dfs_find_nodes<'graph>(
node: &'graph StructDescriptor<'graph>,
graph: &'graph StructGraph<'graph>,
node_set: &mut BTreeSet<StructDescriptor<'graph>>,
) {
node_set.insert(node.clone());
for child in node.children(graph) {
if node_set.contains(child) {
continue;
}
dfs_find_nodes(child, graph, node_set);
}
}
#[derive(Clone, Debug)]
struct StructGraph<'a> {
#[allow(dead_code)]
name_tab: HashMap<String, usize>,
#[allow(dead_code)]
item_offset_tab: Vec<Option<usize>>,
descriptors: Vec<StructDescriptor<'a>>,
}
impl<'a> From<&'a [syn::Item]> for StructGraph<'a> {
fn from(items: &'a [syn::Item]) -> StructGraph<'a> {
let mut descriptors = Vec::new();
let mut name_tab: HashMap<String, usize> = HashMap::new();
let mut item_offset_tab: Vec<Option<usize>> = vec![None; items.len()];
for (i, item) in items.iter().enumerate() {
if let &syn::Item::Struct(struct_) = &item {
let next_offset = descriptors.len();
descriptors.push(StructDescriptor {
struct_,
items_offset: i,
parent: None,
children: Vec::new(),
});
name_tab.insert(struct_.ident.to_string(), next_offset);
item_offset_tab[i] = Some(next_offset);
}
}
for item in items.iter() {
let (id, first_field) = match &item {
syn::Item::Struct(syn::ItemStruct {
ident: id,
fields: syn::Fields::Named(fields),
..
}) => {
if let Some(first_field) = fields.named.first() {
(id.to_string(), first_field)
} else {
continue;
}
}
&syn::Item::Struct(syn::ItemStruct {
ident: id,
fields: syn::Fields::Unnamed(fields),
..
}) => {
if let Some(first_field) = fields.unnamed.first() {
(id.to_string(), first_field)
} else {
continue;
}
}
_ => continue,
};
if let syn::Type::Path(p) = &first_field.ty {
if let Some(last_segment) = p.path.segments.last() {
if let Some(parent_offset) = name_tab.get(&last_segment.ident.to_string()) {
let child_offset = name_tab[&id];
descriptors[child_offset].parent = Some(*parent_offset);
descriptors[*parent_offset].children.push(child_offset);
}
}
}
}
StructGraph { name_tab, item_offset_tab, descriptors }
}
}
impl<'a> StructDescriptor<'a> {
fn children(&'a self, graph: &'a StructGraph) -> StructDescriptorChildren {
StructDescriptorChildren { offset: 0, descriptor: self, graph }
}
}
struct StructDescriptorChildren<'a> {
offset: usize,
descriptor: &'a StructDescriptor<'a>,
graph: &'a StructGraph<'a>,
}
impl<'a> std::iter::Iterator for StructDescriptorChildren<'a> {
type Item = &'a StructDescriptor<'a>;
fn next(&mut self) -> Option<&'a StructDescriptor<'a>> {
if self.offset >= self.descriptor.children.len() {
None
} else {
let ret = Some(&self.graph.descriptors[self.descriptor.children[self.offset]]);
self.offset += 1;
ret
}
}
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
struct StructDescriptor<'a> {
struct_: &'a syn::ItemStruct,
items_offset: usize,
parent: Option<usize>,
children: Vec<usize>,
}
impl PartialOrd for StructDescriptor<'_> {
#[inline]
fn partial_cmp(&self, other: &StructDescriptor) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for StructDescriptor<'_> {
#[inline]
fn cmp(&self, other: &StructDescriptor) -> Ordering {
self.struct_.ident.cmp(&other.struct_.ident)
}
}
fn get_bindings(
major_version: u16,
pg_config: &PgConfig,
include_h: &path::Path,
) -> eyre::Result<syn::File> {
let bindings = if let Some(info_dir) =
target_env_tracked(&format!("PGRX_TARGET_INFO_PATH_PG{major_version}"))
{
let bindings_file = format!("{info_dir}/pg{major_version}_raw_bindings.rs");
std::fs::read_to_string(&bindings_file)
.wrap_err_with(|| format!("failed to read raw bindings from {bindings_file}"))?
} else {
let bindings = run_bindgen(major_version, pg_config, include_h)?;
if let Some(path) = env_tracked("PGRX_PG_SYS_EXTRA_OUTPUT_PATH") {
std::fs::write(path, &bindings)?;
}
bindings
};
syn::parse_file(bindings.as_str()).wrap_err_with(|| "failed to parse generated bindings")
}
fn run_bindgen(
major_version: u16,
pg_config: &PgConfig,
include_h: &path::Path,
) -> eyre::Result<String> {
eprintln!("Generating bindings for pg{major_version}");
let configure = pg_config.configure()?;
let preferred_clang: Option<&std::path::Path> = configure.get("CLANG").map(|s| s.as_ref());
eprintln!("pg_config --configure CLANG = {preferred_clang:?}");
let (autodetect, includes) = build::clang::detect_include_paths_for(preferred_clang);
let mut binder = bindgen::Builder::default();
binder = add_blocklists(binder);
binder = add_derives(binder);
if !autodetect {
let builtin_includes = includes.iter().filter_map(|p| Some(format!("-I{}", p.to_str()?)));
binder = binder.clang_args(builtin_includes);
};
let enum_names = Rc::new(RefCell::new(BTreeMap::new()));
let overrides = BindingOverride::new_from(Rc::clone(&enum_names));
let bindings = binder
.header(include_h.display().to_string())
.clang_args(extra_bindgen_clang_args(pg_config)?)
.clang_args(pg_target_include_flags(major_version, pg_config)?)
.detect_include_paths(autodetect)
.parse_callbacks(Box::new(overrides))
.default_enum_style(bindgen::EnumVariation::ModuleConsts)
.rustified_non_exhaustive_enum("NodeTag")
.size_t_is_usize(true)
.merge_extern_blocks(true)
.formatter(bindgen::Formatter::None)
.layout_tests(false)
.default_non_copy_union_style(NonCopyUnionStyle::ManuallyDrop)
.generate()
.wrap_err_with(|| format!("Unable to generate bindings for pg{major_version}"))?;
let mut binding_str = bindings.to_string();
drop(bindings);
let enum_names: EnumMap = Rc::into_inner(enum_names).unwrap().into_inner();
binding_str.extend(enum_names.into_iter().flat_map(|(name, variants)| {
const MIN_I32: i64 = i32::MIN as _;
const MAX_I32: i64 = i32::MAX as _;
const MAX_U32: u64 = u32::MAX as _;
variants.into_iter().map(move |(variant, value)| {
let (ty, value) = match value {
EnumVariantValue::Boolean(b) => ("bool", b.to_string()),
EnumVariantValue::Signed(v @ MIN_I32..=MAX_I32) => ("i32", v.to_string()),
EnumVariantValue::Signed(v) => ("i64", v.to_string()),
EnumVariantValue::Unsigned(v @ 0..=MAX_U32) => ("u32", v.to_string()),
EnumVariantValue::Unsigned(v) => ("u64", v.to_string()),
};
format!(
r#"
#[deprecated(since = "0.12.0", note = "you want pg_sys::{module}::{variant}")]
pub const {module}_{variant}: {ty} = {value};"#,
module = &*name, )
})
}));
Ok(binding_str)
}
fn add_blocklists(bind: bindgen::Builder) -> bindgen::Builder {
bind.blocklist_type("Datum") .blocklist_type("Oid") .blocklist_function("varsize_any") .blocklist_function("(?:raw_)?(?:expression|query|query_or_expression)_tree_walker")
.blocklist_function("planstate_tree_walker")
.blocklist_function("range_table_(?:entry_)?walker")
.blocklist_function(".*(?:set|long)jmp")
.blocklist_function("pg_re_throw")
.blocklist_function("err(start|code|msg|detail|context_msg|hint|finish)")
.blocklist_item("CONFIGURE_ARGS") .blocklist_item("_*(?:HAVE|have)_.*") .blocklist_item("_[A-Z_]+_H") .blocklist_item("__[A-Z].*") .blocklist_item("__darwin.*") .blocklist_function("pq(?:Strerror|Get.*)") .blocklist_function("log")
.blocklist_item(".*pthread.*)") .blocklist_item(".*(?i:va)_(?i:list|start|end|copy).*") .blocklist_function("(?:pg_|p)v(?:sn?|f)?printf")
.blocklist_function("appendStringInfoVA")
.blocklist_file("stdarg.h")
.blocklist_function("(?:sigstack|sigreturn|siggetmask|gets|vfork|te?mpnam(?:_r)?|mktemp)")
.blocklist_function("inet_net_pton.*")
}
fn add_derives(bind: bindgen::Builder) -> bindgen::Builder {
bind.derive_debug(true)
.derive_copy(true)
.derive_default(true)
.derive_eq(false)
.derive_partialeq(false)
.derive_hash(false)
.derive_ord(false)
.derive_partialord(false)
}
fn env_tracked(s: &str) -> Option<String> {
const CARGO_KEYS: &[&str] = &[
"BROWSER",
"DEBUG",
"DOCS_RS",
"HOST",
"HTTP_PROXY",
"HTTP_TIMEOUT",
"NUM_JOBS",
"OPT_LEVEL",
"OUT_DIR",
"PATH",
"PROFILE",
"TARGET",
"TERM",
];
let is_cargo_key =
s.starts_with("CARGO") || s.starts_with("RUST") || CARGO_KEYS.binary_search(&s).is_ok();
if !is_cargo_key {
println!("cargo:rerun-if-env-changed={s}");
}
std::env::var(s).ok()
}
fn target_env_tracked(s: &str) -> Option<String> {
let target = env_tracked("TARGET").unwrap();
env_tracked(&format!("{s}_{target}")).or_else(|| env_tracked(s))
}
fn pg_target_include_flags(pg_version: u16, pg_config: &PgConfig) -> eyre::Result<Option<String>> {
let var = "PGRX_INCLUDEDIR_SERVER";
let value =
target_env_tracked(&format!("{var}_PG{pg_version}")).or_else(|| target_env_tracked(var));
match value {
None => Ok(Some(format!("-I{}", pg_config.includedir_server()?.display()))),
Some(overridden) if overridden.is_empty() => Ok(None),
Some(overridden) => Ok(Some(format!("-I{overridden}"))),
}
}
fn build_shim(
shim_src: &path::Path,
shim_dst: &path::Path,
pg_config: &PgConfig,
) -> eyre::Result<()> {
let major_version = pg_config.major_version()?;
build_shim_for_version(shim_src, shim_dst, pg_config)?;
let envvar_name = format!("CARGO_FEATURE_PG{major_version}");
if env_tracked(&envvar_name).is_some() {
println!("cargo:rustc-link-search={}", shim_dst.display());
println!("cargo:rustc-link-lib=static=pgrx-cshim-{major_version}");
}
Ok(())
}
fn build_shim_for_version(
shim_src: &path::Path,
shim_dst: &path::Path,
pg_config: &PgConfig,
) -> eyre::Result<()> {
let path_env = prefix_path(pg_config.parent_path());
let major_version = pg_config.major_version()?;
eprintln!("PATH for build_shim={path_env}");
eprintln!("shim_src={}", shim_src.display());
eprintln!("shim_dst={}", shim_dst.display());
fs::create_dir_all(shim_dst).unwrap();
let makefile_dst = path::Path::join(shim_dst, "./Makefile");
if !makefile_dst.try_exists()? {
fs::copy(path::Path::join(shim_src, "./Makefile"), makefile_dst).unwrap();
}
let cshim_dst = path::Path::join(shim_dst, "./pgrx-cshim.c");
if !cshim_dst.try_exists()? {
fs::copy(path::Path::join(shim_src, "./pgrx-cshim.c"), cshim_dst).unwrap();
}
let make = env_tracked("MAKE").unwrap_or_else(|| "make".to_string());
let rc = run_command(
Command::new(make)
.arg("clean")
.arg(&format!("libpgrx-cshim-{major_version}.a"))
.env("PG_TARGET_VERSION", format!("{major_version}"))
.env("PATH", path_env)
.env_remove("TARGET")
.env_remove("HOST")
.current_dir(shim_dst),
&format!("shim for PG v{major_version}"),
)?;
if rc.status.code().unwrap() != 0 {
return Err(eyre!("failed to make pgrx-cshim for v{major_version}"));
}
Ok(())
}
fn extra_bindgen_clang_args(pg_config: &PgConfig) -> eyre::Result<Vec<String>> {
let mut out = vec![];
if env_tracked("CARGO_CFG_TARGET_OS").as_deref() == Some("macos") {
let flags = pg_config.cppflags()?;
let flags = shlex::split(&flags.to_string_lossy()).unwrap_or_default();
out.extend(flags.iter().cloned());
for pair in flags.windows(2) {
if pair[0] == "-isysroot" {
if !std::path::Path::new(&pair[1]).exists() {
let major_version = pg_config.major_version()?;
println!(
"cargo:warning=postgres v{major_version} was compiled against an \
SDK Root which does not seem to exist on this machine ({}). You may \
need to re-run `cargo pgrx init` and/or update your command line tools.",
pair[1],
);
};
break;
}
}
}
Ok(out)
}
fn run_command(mut command: &mut Command, version: &str) -> eyre::Result<Output> {
let mut dbg = String::new();
command = command
.env_remove("DEBUG")
.env_remove("MAKEFLAGS")
.env_remove("MAKELEVEL")
.env_remove("MFLAGS")
.env_remove("DYLD_FALLBACK_LIBRARY_PATH")
.env_remove("OPT_LEVEL")
.env_remove("PROFILE")
.env_remove("OUT_DIR")
.env_remove("NUM_JOBS");
eprintln!("[{version}] {command:?}");
dbg.push_str(&format!("[{version}] -------- {command:?} -------- \n"));
let output = command.output()?;
let rc = output.clone();
if !output.stdout.is_empty() {
for line in String::from_utf8(output.stdout).unwrap().lines() {
if line.starts_with("cargo:") {
dbg.push_str(&format!("{line}\n"));
} else {
dbg.push_str(&format!("[{version}] [stdout] {line}\n"));
}
}
}
if !output.stderr.is_empty() {
for line in String::from_utf8(output.stderr).unwrap().lines() {
dbg.push_str(&format!("[{version}] [stderr] {line}\n"));
}
}
dbg.push_str(&format!("[{version}] /----------------------------------------\n"));
eprintln!("{dbg}");
Ok(rc)
}
static BLOCKLISTED: OnceLock<BTreeSet<&'static str>> = OnceLock::new();
fn is_blocklisted_item(item: &ForeignItem) -> bool {
let sym_name = match item {
ForeignItem::Fn(f) => &f.sig.ident,
ForeignItem::Static(s) => &s.ident,
_ => return false,
};
BLOCKLISTED
.get_or_init(|| build::sym_blocklist::SYMBOLS.iter().copied().collect::<BTreeSet<&str>>())
.contains(sym_name.to_string().as_str())
}
fn apply_pg_guard(items: &Vec<syn::Item>) -> eyre::Result<proc_macro2::TokenStream> {
let mut out = proc_macro2::TokenStream::new();
for item in items {
match item {
Item::ForeignMod(block) => {
let abi = &block.abi;
let (mut extern_funcs, mut others) = (Vec::new(), Vec::new());
block.items.iter().filter(|&item| !is_blocklisted_item(item)).cloned().for_each(
|item| match item {
ForeignItem::Fn(func) => extern_funcs.push(func),
item => others.push(item),
},
);
out.extend(quote! {
#[pgrx_macros::pg_guard]
#abi { #(#extern_funcs)* }
});
out.extend(quote! { #abi { #(#others)* } });
}
_ => {
out.extend(item.into_token_stream());
}
}
}
Ok(out)
}
fn rust_fmt(path: &Path) -> eyre::Result<()> {
let rustfmt = env_tracked("RUSTFMT").unwrap_or_else(|| "rustfmt".into());
let out = run_command(Command::new(rustfmt).arg(path).current_dir("."), "[bindings_diff]");
match out {
Ok(_) => Ok(()),
Err(e)
if e.downcast_ref::<std::io::Error>()
.ok_or(eyre!("Couldn't downcast error ref"))?
.kind()
== std::io::ErrorKind::NotFound =>
{
Err(e).wrap_err("Failed to run `rustfmt`, is it installed?")
}
Err(e) => Err(e),
}
}