Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: parsing of build backend configuration #2624

Merged
merged 8 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 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 @@ -20,6 +20,7 @@ async-fd-lock = "0.2.0"
async-once-cell = "0.5.3"
async-trait = "0.1.82"
base64 = "0.22.1"
bytes = "1.9.0"
chrono = "0.4.38"
clap = { version = "4.5.9", default-features = false }
clap-verbosity-flag = "2.2.0"
Expand Down Expand Up @@ -86,6 +87,7 @@ tar = "0.4.40"
tempfile = "3.10.1"
thiserror = "1.0.58"
tokio = "1.37.0"
tokio-stream = "0.1.16"
tokio-util = "0.7.10"
toml_edit = "0.22.11"
tracing = "0.1.40"
Expand Down
4 changes: 4 additions & 0 deletions crates/pixi_build_frontend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ which = { workspace = true }

pixi_build_types = { path = "../pixi_build_types" }


[dev-dependencies]
bytes = { workspace = true }
insta = { workspace = true, features = ["yaml", "filters"] }
rstest = { workspace = true }
tempfile = { workspace = true }
Expand All @@ -52,3 +54,5 @@ tokio = { workspace = true, features = [
"io-std",
"rt-multi-thread",
] }
tokio-stream = { workspace = true }
tokio-util = { workspace = true, features = ["io"] }
31 changes: 27 additions & 4 deletions crates/pixi_build_frontend/src/protocols/builders/pixi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use miette::Diagnostic;
use pixi_consts::consts;
use pixi_manifest::{Manifest, PackageManifest, PrioritizedChannel, WorkspaceManifest};
use rattler_conda_types::{ChannelConfig, MatchSpec};
use serde::{de::IntoDeserializer, Deserialize};
use thiserror::Error;
use which::Error;

Expand Down Expand Up @@ -78,16 +79,16 @@ impl ProtocolBuilder {
manifest_path: PathBuf,
workspace_manifest: WorkspaceManifest,
package_manifest: PackageManifest,
) -> Result<Self, ProtocolBuildError> {
Ok(Self {
) -> Self {
Self {
source_dir,
manifest_path,
workspace_manifest,
package_manifest,
override_backend_spec: None,
channel_config: None,
cache_dir: None,
})
}
}

/// Sets an optional backend override.
Expand Down Expand Up @@ -128,7 +129,7 @@ impl ProtocolBuilder {
manifest_path,
manifest.workspace,
package_manifest,
)?;
);
return Ok(Some(builder));
}
Err(e) => {
Expand Down Expand Up @@ -195,9 +196,20 @@ impl ProtocolBuilder {
.await
.map_err(FinishError::Tool)?;

let configuration = self
.package_manifest
.build_system
.build_backend
.additional_args
.map_or(serde_json::Value::Null, |value| {
let deserializer = value.into_deserializer();
serde_json::Value::deserialize(deserializer).unwrap_or(serde_json::Value::Null)
});

Ok(JsonRPCBuildProtocol::setup(
self.source_dir,
self.manifest_path,
configuration,
build_id,
self.cache_dir,
tool,
Expand All @@ -212,10 +224,21 @@ impl ProtocolBuilder {
ipc: InProcessBackend,
build_id: usize,
) -> Result<JsonRPCBuildProtocol, FinishError> {
let configuration = self
.package_manifest
.build_system
.build_backend
.additional_args
.map_or(serde_json::Value::Null, |value| {
let deserializer = value.into_deserializer();
serde_json::Value::deserialize(deserializer).unwrap_or(serde_json::Value::Null)
});

Ok(JsonRPCBuildProtocol::setup_with_transport(
"<IPC>".to_string(),
self.source_dir,
self.manifest_path,
configuration,
build_id,
self.cache_dir,
Sender::from(ipc.rpc_out),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ impl ProtocolBuilder {
Ok(JsonRPCBuildProtocol::setup(
self.source_dir,
self.recipe_dir.join("recipe.yaml"),
serde_json::Value::Null,
build_id,
self.cache_dir,
tool,
Expand Down
4 changes: 4 additions & 0 deletions crates/pixi_build_frontend/src/protocols/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ impl JsonRPCBuildProtocol {
async fn setup(
source_dir: PathBuf,
manifest_path: PathBuf,
configuration: serde_json::Value,
build_id: usize,
cache_dir: Option<PathBuf>,
tool: Tool,
Expand Down Expand Up @@ -177,6 +178,7 @@ impl JsonRPCBuildProtocol {
backend_identifier,
source_dir,
manifest_path,
configuration,
build_id,
cache_dir,
tx,
Expand All @@ -193,6 +195,7 @@ impl JsonRPCBuildProtocol {
source_dir: PathBuf,
// In case of rattler-build it's recipe.yaml
manifest_path: PathBuf,
configuration: serde_json::Value,
build_id: usize,
cache_dir: Option<PathBuf>,
sender: impl TransportSenderT + Send,
Expand All @@ -212,6 +215,7 @@ impl JsonRPCBuildProtocol {
manifest_path: manifest_path.clone(),
capabilities: FrontendCapabilities {},
cache_directory: cache_dir,
configuration,
}),
)
.await
Expand Down
116 changes: 105 additions & 11 deletions crates/pixi_build_frontend/tests/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
use std::path::Path;

use bytes::Bytes;
use futures::{SinkExt, StreamExt};
use jsonrpsee::types::Request;
use miette::{Diagnostic, GraphicalReportHandler, GraphicalTheme};
use pixi_build_frontend::{BuildFrontend, InProcessBackend, SetupRequest};
use pixi_build_types::procedures::initialize::InitializeParams;
use pixi_manifest::toml::{ExternalWorkspaceProperties, TomlManifest};
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, BufReader};
use tokio_stream::wrappers::ReceiverStream;
use tokio_util::{
io::{CopyToBytes, SinkWriter, StreamReader},
sync::PollSender,
};

fn error_to_snapshot(diag: &impl Diagnostic) -> String {
let mut report_str = String::new();
Expand Down Expand Up @@ -130,34 +140,118 @@ async fn test_invalid_backend() {
build-backend = { name = "ipc", version = "*" }
"#;

tokio::fs::write(&manifest, toml).await.unwrap();

let (in_tx, in_rx) = tokio::io::duplex(1024);
let (out_tx, _out_rx) = tokio::io::duplex(1024);
let (frontend_tx, backend_rx) = pipe();
let (backend_tx, frontend_rx) = pipe();
let ipc = InProcessBackend {
rpc_in: Box::new(in_rx),
rpc_out: Box::new(out_tx),
rpc_in: Box::new(frontend_rx),
rpc_out: Box::new(frontend_tx),
};

// Explicitly drop the sending end of the channel to simulate a closed
// connection.
drop(in_tx);
drop(backend_rx);
drop(backend_tx);

let (workspace, package) = TomlManifest::from_toml_str(toml)
.unwrap()
.into_manifests(ExternalWorkspaceProperties::default())
.unwrap();
let protocol = pixi_build_frontend::pixi_protocol::ProtocolBuilder::new(
let err = pixi_build_frontend::pixi_protocol::ProtocolBuilder::new(
source_dir.path().to_path_buf(),
manifest.to_path_buf(),
workspace,
package.unwrap(),
)
.unwrap();

let err = protocol.finish_with_ipc(ipc, 0).await.unwrap_err();
.finish_with_ipc(ipc, 0)
.await
.unwrap_err();

let snapshot = error_to_snapshot(&err);
let snapshot = replace_source_dir(&snapshot, source_dir.path());
insta::assert_snapshot!(snapshot);
}

#[tokio::test]
async fn test_backend_configuration() {
let toml = r#"
[workspace]
platforms = []
channels = []
preview = ['pixi-build']

[package]
version = "0.1.0"
name = "project"

[build-system]
build-backend = { name = "ipc", version = "*" }

[build-backend.ipc]
hello = "world"
"#;

let source_dir = tempfile::TempDir::new().unwrap();
let manifest = source_dir
.path()
.join(pixi_consts::consts::PROJECT_MANIFEST);

let (frontend_tx, backend_rx) = pipe();
let (backend_tx, frontend_rx) = pipe();
let ipc = InProcessBackend {
rpc_in: Box::new(frontend_rx),
rpc_out: Box::new(frontend_tx),
};

let protocol_setup = tokio::spawn(async move {
let (workspace, package) = TomlManifest::from_toml_str(toml)
.unwrap()
.into_manifests(ExternalWorkspaceProperties::default())
.unwrap();
pixi_build_frontend::pixi_protocol::ProtocolBuilder::new(
source_dir.path().to_path_buf(),
manifest.to_path_buf(),
workspace,
package.unwrap(),
)
.finish_with_ipc(ipc, 0)
.await
.expect_err("the test never sends a response to the initialize request");
});

let read_initialize_message = async move {
let initialize_line = BufReader::new(backend_rx)
.lines()
.next_line()
.await
.unwrap()
.unwrap();
let request: Request = serde_json::from_str(&initialize_line).unwrap();
let init_params: InitializeParams = request.params().parse().unwrap();
drop(backend_tx); // Simulates the backend closing the connection.
init_params
};

let (_, init_params) = tokio::join!(protocol_setup, read_initialize_message);

insta::assert_snapshot!(serde_json::to_string_pretty(&init_params.configuration).unwrap());
}

/// Creates a pipe that connects an async write instance to an async read
/// instance.
pub fn pipe() -> (
impl AsyncWrite + Unpin + Send,
impl AsyncRead + Unpin + Send,
) {
let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(1);

// Convert the sender into an async write instance
let sink =
PollSender::new(tx).sink_map_err(|_| std::io::Error::from(std::io::ErrorKind::BrokenPipe));
let writer = SinkWriter::new(CopyToBytes::new(sink));

// Convert the receiver into an async read instance
let stream = ReceiverStream::new(rx).map(Ok::<_, std::io::Error>);
let reader = StreamReader::new(stream);

(writer, reader)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: crates/pixi_build_frontend/tests/diagnostics.rs
expression: "serde_json::to_string_pretty(&init_params.configuration).unwrap()"
---
{
"hello": "world"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ source: crates/pixi_build_frontend/tests/diagnostics.rs
expression: snapshot
---
× failed to communicate with the build backend (<IPC>)
╰─▶ The background task closed EOF; restart required
╰─▶ The background task closed broken pipe; restart required
help: Ensure that the build backend implements the JSON-RPC protocol correctly.
1 change: 1 addition & 0 deletions crates/pixi_build_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ version = "0.1.0"
[dependencies]
rattler_conda_types = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_with = { workspace = true }
url = { workspace = true }
5 changes: 5 additions & 0 deletions crates/pixi_build_types/src/procedures/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ pub const METHOD_NAME: &str = "initialize";
pub struct InitializeParams {
/// The manifest that the build backend should use.
pub manifest_path: PathBuf,

/// Additional configuration to configure the backend. This configuration is
/// specific to the backend.
pub configuration: serde_json::Value,

/// The capabilities that the frontend provides.
pub capabilities: FrontendCapabilities,

Expand Down
4 changes: 4 additions & 0 deletions crates/pixi_manifest/src/build_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ pub struct BuildBackend {

/// The spec for the backend
pub spec: BinarySpec,

/// Additional arguments to pass to the build backend. In the manifest these are read from the
/// `[build-backend]` section.
pub additional_args: Option<serde_value::Value>,
}

impl BuildSystem {
Expand Down
3 changes: 3 additions & 0 deletions crates/pixi_manifest/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ pub enum TomlError {
MissingField(Cow<'static, str>, Option<Range<usize>>),
#[error("{0}")]
Generic(Cow<'static, str>, Option<Range<usize>>),
#[error("{0}")]
GenericLabels(Cow<'static, str>, Vec<LabeledSpan>),
#[error(transparent)]
FeatureNotEnabled(#[from] FeatureNotEnabled),
#[error("Could not find or access the part '{part}' in the path '[{table_name}]'")]
Expand Down Expand Up @@ -114,6 +116,7 @@ impl Diagnostic for TomlError {
TomlError::Generic(_, span) | TomlError::MissingField(_, span) => {
span.clone().map(SourceSpan::from)
}
TomlError::GenericLabels(_, spans) => return Some(Box::new(spans.clone().into_iter())),
TomlError::FeatureNotEnabled(err) => return err.labels(),
TomlError::InvalidNonPackageDependencies(err) => return err.labels(),
_ => None,
Expand Down
Loading
Loading