use std::collections::BTreeMap;
use std::path::PathBuf;
use std::{env, process};
#[derive(Default, Clone, Debug)]
pub struct Cargo {
subcmd: String,
features: clap_cargo::Features,
stdio: [Stdio; 3],
manifest: Option<PathBuf>,
package: String,
log_level: Option<String>,
target: Option<String>,
profile: CargoProfile,
more_args: BTreeMap<String, Vec<String>>,
rustc_args: Vec<String>,
}
impl Cargo {
pub fn subcommand(mut self, s: &str) -> Self {
self.subcmd.clear();
self.subcmd.push_str(s);
self
}
pub fn std_streams(mut self, streams: [Stdio; 3]) -> Self {
self.stdio = streams;
self
}
pub fn manifest_path(mut self, path: Option<PathBuf>) -> Self {
self.manifest = path;
self
}
pub fn package(mut self, package: String) -> Self {
self.package = package;
self
}
pub fn target(mut self, target: Option<String>) -> Self {
self.target = target;
self
}
pub fn profile(mut self, profile: CargoProfile) -> Self {
self.profile = profile;
self
}
pub fn log_level(mut self, level: Option<String>) -> Self {
self.log_level = level;
self
}
pub fn flag(mut self, flag: impl Into<String>) -> Self {
self.more_args.insert(flag.into(), Vec::new());
self
}
pub fn features(mut self, features: clap_cargo::Features) -> Self {
self.features = features;
self
}
pub fn rustc_args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.rustc_args.extend(args.into_iter().map(Into::into));
self
}
#[track_caller]
pub fn into_command(self) -> process::Command {
if !self.rustc_args.is_empty() && self.subcmd != "rustc" {
panic!("`Cargo::rustc_args` requires the `rustc` subcommand, was: {self:?}");
}
let mut cmd = cargo();
if !self.subcmd.is_empty() {
cmd.arg(&self.subcmd);
} else {
panic!("`Cargo::into_command` requires a subcommand to be set, was: {self:?}")
}
let Self {
features,
stdio,
manifest,
log_level,
target,
profile,
package,
subcmd: _,
more_args,
rustc_args,
} = self;
cmd.args(profile.cargo_args());
if let Some(target) = target {
cmd.arg("--target").arg(target);
}
if let Some(manifest) = manifest {
cmd.arg("--manifest-path").arg(manifest);
}
if !package.is_empty() {
cmd.arg("--package").arg(package);
}
let [stdin, stdout, stderr] = stdio;
if let Some(stdio) = stdin.into_stdio() {
cmd.stdin(stdio);
}
if let Some(stdio) = stdout.into_stdio() {
cmd.stdout(stdio);
}
if let Some(stdio) = stderr.into_stdio() {
cmd.stderr(stdio);
}
if features.no_default_features {
cmd.arg("--no-default-features");
}
if features.all_features {
cmd.arg("--all-features");
}
if !features.features.is_empty() {
cmd.arg("--features");
cmd.arg(features.features.join(" "));
}
let flags = env::var("PGRX_BUILD_FLAGS").unwrap_or_default();
for arg in flags.split_ascii_whitespace() {
cmd.arg(arg);
}
if let Some(log_level) = log_level {
cmd.env("RUST_LOG", log_level);
}
for (flag, args) in more_args {
cmd.arg(flag).args(args);
}
if !rustc_args.is_empty() {
cmd.arg("--").args(rustc_args);
}
cmd
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Stdio {
Inherit,
Null,
#[default]
Default,
}
impl Stdio {
fn into_stdio(self) -> Option<process::Stdio> {
match self {
Self::Inherit => Some(process::Stdio::inherit()),
Self::Null => Some(process::Stdio::null()),
Self::Default => None,
}
}
}
pub(crate) fn cargo() -> std::process::Command {
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
std::process::Command::new(cargo)
}
pub(crate) fn pgrx_cdylib_rustc_args(target: Option<&str>) -> Vec<String> {
if std::env::var_os("CARGO_PGRX_DISABLE_GC_SECTIONS_WORKAROUND").is_some() {
return Vec::new();
}
if !target_uses_gnuish_ld(target) {
return Vec::new();
}
vec!["-C".to_string(), "link-arg=-Wl,--no-gc-sections".to_string()]
}
fn target_uses_gnuish_ld(target: Option<&str>) -> bool {
let Some(target) = target else {
return cfg!(all(target_family = "unix", not(target_os = "macos")));
};
target.split('-').any(|part| {
matches!(
part,
"android"
| "dragonfly"
| "freebsd"
| "hurd"
| "illumos"
| "linux"
| "netbsd"
| "openbsd"
| "solaris"
)
})
}
pub(crate) fn initialize() {
match (std::env::var_os("CARGO_PGRX"), std::env::current_exe()) {
(None, Ok(path)) => {
unsafe {
std::env::set_var("CARGO_PGRX", path);
}
}
(Some(_), Ok(_)) => {
}
(_, Err(_)) => {}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum CargoProfile {
#[default]
Dev,
Release,
Profile(String),
}
impl CargoProfile {
pub fn from_flags(profile: Option<&str>, default: Self) -> eyre::Result<Self> {
match profile {
Some("release") => Ok(Self::Release),
Some("debug") | Some("dev") => Ok(Self::Dev),
Some(profile) => Ok(Self::Profile(profile.into())),
None => Ok(default),
}
}
pub fn cargo_args(&self) -> Vec<String> {
match self {
Self::Dev => vec![],
Self::Release => vec!["--release".into()],
Self::Profile(p) => vec!["--profile".into(), p.into()],
}
}
pub fn name(&self) -> &str {
match self {
Self::Dev => "dev",
Self::Release => "release",
Self::Profile(p) => p,
}
}
pub fn target_subdir(&self) -> &str {
match self {
Self::Dev => "debug",
Self::Release => "release",
Self::Profile(p) => p,
}
}
}
#[cfg(test)]
mod tests {
use super::{Cargo, pgrx_cdylib_rustc_args};
#[test]
fn cdylib_rustc_args_carry_no_gc_sections_workaround() {
if std::env::var_os("CARGO_PGRX_DISABLE_GC_SECTIONS_WORKAROUND").is_some() {
return;
}
let args = pgrx_cdylib_rustc_args(Some("x86_64-unknown-linux-gnu"));
assert_eq!(
args,
["-C", "link-arg=-Wl,--no-gc-sections"],
"cdylib rustc args should disable section GC on GNU-ish Unix"
);
}
#[test]
fn cdylib_rustc_args_skip_non_gnuish_targets() {
assert!(pgrx_cdylib_rustc_args(Some("aarch64-apple-darwin")).is_empty());
assert!(pgrx_cdylib_rustc_args(Some("x86_64-pc-windows-msvc")).is_empty());
}
#[test]
fn cargo_rustc_args_are_not_cargo_config() {
if std::env::var_os("CARGO_PGRX_DISABLE_GC_SECTIONS_WORKAROUND").is_some() {
return;
}
let command = Cargo::default()
.subcommand("rustc")
.flag("--lib")
.rustc_args(pgrx_cdylib_rustc_args(Some("x86_64-unknown-linux-gnu")))
.into_command();
let args = command.get_args().map(|arg| arg.to_string_lossy()).collect::<Vec<_>>();
assert_eq!(args.first().map(|arg| arg.as_ref()), Some("rustc"));
assert!(
!args.iter().any(|arg| arg.as_ref() == "--config"),
"the cdylib workaround must not override Cargo config rustflags: {args:?}"
);
assert!(
args.windows(3).any(|window| {
window[0].as_ref() == "--"
&& window[1].as_ref() == "-C"
&& window[2].as_ref() == "link-arg=-Wl,--no-gc-sections"
}),
"expected no-GC rustc args after `--`: {args:?}"
);
}
}