Skip to content

Commit

Permalink
Implemented pipeline detection for PGXS and pgrx
Browse files Browse the repository at this point in the history
The PGXS pipeline examines the `Makefile` (via regular expressions),
while the pgrx pipeline examines `Cargo.toml` (via the cargo_toml
crate). Each produces a score (0-255), and the highest score wins. If
all pipeline candidates return 0 it returns an error. Now Build::new
handles a pipeline name specified in the release metadata, while
Build::detect figures out which to use when none is specified.

Builder::new now takes a `sudo` param, to indicate whether `sudo` and
passes it to Build::new and Build::detect.

Other tweaks:

*   Added an `install` method to the Builder, the Pipeline trait, and
    placeholder implementations in the Pgrx and Pgxs implementations.
*   Removed the `download` and `unpack` methods from the Builder,
    Pipeline trait, and implementations, as the api::Api struct handles
    this functionality.
  • Loading branch information
theory committed Nov 21, 2024
1 parent 57d7b8e commit 5a82cfa
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .ci/test-cover
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ grcov "${DESTDIR}" \
--ignore 'build.rs' \
--ignore 'target/**' \
--excl-start '#(\[cfg\(test\)\]|\[test\])' \
--excl-line 'unreachable\!\(\)' \
--excl-line 'unreachable\!\(' \
--llvm \
--binary-path "target/debug/" \
-s . \
Expand Down
64 changes: 64 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ exclude = [ ".github", ".vscode", ".gitignore", ".ci", ".pre-*.yaml"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
cargo_toml = "0.20.5"
chrono = "0.4.38"
hex = "0.4.3"
iri-string = "0.7.7"
log = { version = "0.4.22", features = ["kv"] }
pgxn_meta = "0.5.1"
regex = "1.11.1"
semver = "1.0.23"
serde = "1.0.215"
serde_json = "1.0.133"
Expand Down
4 changes: 4 additions & 0 deletions src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ pub enum BuildError {
#[error("unknown build pipeline `{0}`")]
UnknownPipeline(String),

/// Unable to detect the pipeline.
#[error("cannot detect build pipeline and none specified")]
NoPipeline(),

/// IO error.
#[error(transparent)]
Io(#[from] io::Error),
Expand Down
64 changes: 54 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mod pipeline;

use crate::{error::BuildError, pgrx::Pgrx, pgxs::Pgxs, pipeline::Pipeline};
use pgxn_meta::{dist, release::Release};
use std::path::Path;
use std::path::{Path, PathBuf};

/// Defines the types of builders.
#[derive(Debug, PartialEq)]
Expand All @@ -27,6 +27,46 @@ enum Build {
Pgrx(Pgrx),
}

impl Build {
/// Returns a build pipeline identified by `pipe`, or an error if `pipe`
/// is unknown.
fn new(pipe: &dist::Pipeline, dir: PathBuf, sudo: bool) -> Result<Build, BuildError> {
match pipe {
dist::Pipeline::Pgxs => Ok(Build::Pgxs(Pgxs::new(dir, sudo))),
dist::Pipeline::Pgrx => Ok(Build::Pgrx(Pgrx::new(dir, sudo))),
_ => Err(BuildError::UnknownPipeline(pipe.to_string())),
}
}

/// Attempts to detect and return the appropriate build pipeline to build
/// the contents of `dir`. Returns an error if no pipeline can do so.
fn detect(dir: PathBuf, sudo: bool) -> Result<Build, BuildError> {
// Start with PGXS.
let mut score = Pgxs::confidence(&dir);
let mut pipe = dist::Pipeline::Pgxs;

// Does pgrx have a higher score?
let c = Pgrx::confidence(&dir);
if c > score {
score = c;
pipe = dist::Pipeline::Pgrx;
}

// Try each of the others as they're added.
// Return an error if no confidence.
if score == 0 {
return Err(BuildError::NoPipeline());
}

// Construct the winner.
match pipe {
dist::Pipeline::Pgrx => Ok(Build::Pgrx(Pgrx::new(dir, sudo))),
dist::Pipeline::Pgxs => Ok(Build::Pgxs(Pgxs::new(dir, sudo))),
_ => unreachable!("unknown pipelines {pipe}"),
}
}
}

/// Builder builds PGXN releases.
#[derive(Debug, PartialEq)]
pub struct Builder {
Expand All @@ -36,20 +76,16 @@ pub struct Builder {

impl Builder {
/// Creates and returns a new builder using the appropriate pipeline.
pub fn new<P: AsRef<Path>>(dir: P, meta: Release) -> Result<Self, BuildError> {
pub fn new<P: AsRef<Path>>(dir: P, meta: Release, sudo: bool) -> Result<Self, BuildError> {
let dir = dir.as_ref().to_path_buf();
let pipeline = if let Some(deps) = meta.dependencies() {
if let Some(pipe) = deps.pipeline() {
let dir = dir.as_ref().to_path_buf();
match pipe {
dist::Pipeline::Pgxs => Build::Pgxs(Pgxs::new(dir, true)),
dist::Pipeline::Pgrx => Build::Pgrx(Pgrx::new(dir, true)),
_ => return Err(BuildError::UnknownPipeline(pipe.to_string())),
}
Build::new(pipe, dir, sudo)?
} else {
todo!("Detect pipeline");
Build::detect(dir, sudo)?
}
} else {
todo!("Detect pipeline");
Build::detect(dir, sudo)?
};

Ok(Builder { pipeline, meta })
Expand Down Expand Up @@ -79,6 +115,14 @@ impl Builder {
Build::Pgrx(pgrx) => pgrx.test(),
}
}

/// Installs a distribution on a particular platform and Postgres version.
pub fn install(&self) -> Result<(), BuildError> {
match &self.pipeline {
Build::Pgxs(pgxs) => pgxs.install(),
Build::Pgrx(pgrx) => pgrx.install(),
}
}
}

#[cfg(test)]
Expand Down
35 changes: 34 additions & 1 deletion src/pgrx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
use crate::error::BuildError;
use crate::pipeline::Pipeline;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

#[cfg(test)]
mod tests;

/// Builder implementation for [pgrx] Pipelines.
///
Expand All @@ -20,15 +23,45 @@ impl Pipeline for Pgrx {
Pgrx { dir, sudo }
}

/// Determines the confidence that the Pgrx pipeline can build the
/// contents of `dir`. Returns 255 if it contains a file named
/// `Cargo.toml` and lists pgrx as a dependency. Otherwise returns 1 if
/// `Cargo.toml` exists and 0 if it does not.
fn confidence(dir: &Path) -> u8 {
let file = dir.join("Cargo.toml");
if !file.exists() {
return 0;
}

// Does Cargo.toml mention pgrx?
if let Ok(cargo) = cargo_toml::Manifest::from_path(file) {
if cargo.dependencies.contains_key("pgrx") {
// Full confidence
return 255;
}
}

// Have Cargo.toml but no dependence on pgrx. Weak confidence.
1
}

/// Runs `cargo init`.
fn configure(&self) -> Result<(), BuildError> {
Ok(())
}

/// Runs `cargo build`.
fn compile(&self) -> Result<(), BuildError> {
Ok(())
}

/// Runs `cargo test`.
fn test(&self) -> Result<(), BuildError> {
Ok(())
}

/// Runs `cargo install`.
fn install(&self) -> Result<(), BuildError> {
Ok(())
}
}
52 changes: 51 additions & 1 deletion src/pgxs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@
use crate::error::BuildError;
use crate::pipeline::Pipeline;
use std::path::PathBuf;
use regex::Regex;
use std::{
fs::File,
io::{BufRead, BufReader},
path::{Path, PathBuf},
};

#[cfg(test)]
mod tests;

/// Builder implementation for [PGXS] Pipelines.
///
Expand All @@ -20,6 +28,44 @@ impl Pipeline for Pgxs {
Pgxs { dir, sudo }
}

/// Determines the confidence that the Pgxs pipeline can build the
/// contents of `dir`. Returns 0 unless the directory contains a file
/// named `Makefile`. Otherwise it returns a score as follows;
///
/// * Returns 255 if it declares a variable named `PG_CONFIG`.
/// * Returns 200 if it declares variables named `MODULES`,
/// `MODULE_big`, `PROGRAM`, `EXTENSION`, `DATA`, or `DATA_built`
/// * Otherwise returns 127
fn confidence(dir: &Path) -> u8 {
let file = dir.join("Makefile");
if !file.exists() {
return 0;
}

// https://www.postgresql.org/docs/current/extend-pgxs.html
// https://github.com/postgres/postgres/blob/master/src/makefiles/pgxs.mk
if let Ok(file) = File::open(file) {
let reader = BufReader::new(file);
let pgc_rx = Regex::new(r"^PG_CONFIG\s*[:?]?=\s*").unwrap();
let var_rx =
Regex::new(r"^(MODULE(?:S|_big)|PROGRAM|EXTENSION|DATA(?:_built)?)\s*[:?]?=")
.unwrap();
for line in reader.lines().map_while(Result::ok) {
if pgc_rx.is_match(&line) {
// Full confidence
return 255;
}
if var_rx.is_match(&line) {
// Probably
return 200;
}
}
}

// Probably can do `make all && make install`, probably not `installcheck`.
127
}

fn configure(&self) -> Result<(), BuildError> {
Ok(())
}
Expand All @@ -31,4 +77,8 @@ impl Pipeline for Pgxs {
fn test(&self) -> Result<(), BuildError> {
Ok(())
}

fn install(&self) -> Result<(), BuildError> {
Ok(())
}
}
12 changes: 10 additions & 2 deletions src/pipeline/mod.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
//! Build Pipeline interface definition.
use crate::error::BuildError;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

/// Defines the interface for build pipelines to configure, compile, and test
/// PGXN distributions.
pub(crate) trait Pipeline {
/// Creates an instance of a Builder.
/// Creates an instance of a Pipeline.
fn new(dir: PathBuf, sudo: bool) -> Self;

/// Returns a score for the confidence that this pipeline can build the
/// contents of `dir`. A score of 0 means no confidence and 255 means the
/// highest confidence.
fn confidence(dir: &Path) -> u8;

/// Configures a distribution to build on a particular platform and
/// Postgres version.
fn configure(&self) -> Result<(), BuildError>;

/// Compiles a distribution on a particular platform and Postgres version.
fn compile(&self) -> Result<(), BuildError>;

/// Installs a distribution on a particular platform and Postgres version.
fn install(&self) -> Result<(), BuildError>;

/// Tests a distribution a particular platform and Postgres version.
fn test(&self) -> Result<(), BuildError>;
}
Loading

0 comments on commit 5a82cfa

Please sign in to comment.