Move daemon to its own module

This commit is contained in:
Zanie 2024-01-17 09:20:28 -06:00
parent 744b546a85
commit a8fa91bfc5
2 changed files with 406 additions and 391 deletions

View File

@ -0,0 +1,401 @@
use std::env;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::process::{Output, Stdio};
use std::str::FromStr;
use tokio::fs::File;
use tokio::io::BufReader;
use tokio::io::{AsyncWriteExt, Lines};
use tokio::process::{Child, ChildStdin, ChildStdout, Command};
use tracing::{debug, error};
use crate::{BuildKind, Error, Pep517Backend};
use pep508_rs::Requirement;
use puffin_interpreter::Virtualenv;
use puffin_traits::SourceBuildTrait;
static HOOKD_SOURCE: &'static str = include_str!("hookd.py");
#[derive(Debug, Eq, PartialEq, Clone)]
enum Pep517DaemonResponse {
Debug(String),
Error(Pep517DaemonErrorKind, String),
Traceback(String),
Ok(String),
Stderr(PathBuf),
Stdout(PathBuf),
Expect(String),
Ready,
Fatal(String, String),
Shutdown,
}
impl Pep517DaemonResponse {
fn from_line(line: &str) -> Result<Self, Error> {
// Split on the first two spaces
let mut parts = line.splitn(3, ' ');
if let Some(kind) = parts.next() {
let response = match kind {
"DEBUG" => Self::Debug(parts.collect::<Vec<&str>>().join(" ")),
"EXPECT" => Self::Expect(parts.collect::<Vec<&str>>().join(" ")),
"OK" => Self::Ok(parts.collect::<Vec<&str>>().join(" ")),
"TRACEBACK" => Self::Traceback(parts.collect::<Vec<&str>>().join(" ")),
"ERROR" => Self::Error(
Pep517DaemonErrorKind::from_str(parts.next().unwrap())?,
parts.next().unwrap().to_string(),
),
"STDOUT" => Self::Stdout(parts.next().unwrap().into()),
"STDERR" => Self::Stderr(parts.next().unwrap().into()),
"READY" => Self::Ready,
"FATAL" => Self::Fatal(
parts.next().unwrap().to_string(),
parts.next().unwrap().to_string(),
),
"SHUTDOWN" => Self::Shutdown,
_ => {
return Err(Error::DaemonError {
message: format!("Unknown response: {}", line),
})
}
};
Ok(response)
} else {
Err(Error::DaemonError {
message: "No kind in response.".into(),
})
}
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
enum Pep517DaemonErrorKind {
MissingBackendModule,
MissingBackendAttribute,
MalformedBackendName,
BackendImportError,
InvalidHookName,
InvalidAction,
UnsupportedHook,
MalformedHookArgument,
HookRuntimeError,
}
impl Pep517DaemonErrorKind {
fn from_str(name: &str) -> Result<Self, Error> {
match name {
"MissingBackendModule" => Ok(Self::MissingBackendModule),
"MissingBackendAttribute" => Ok(Self::MissingBackendAttribute),
"MalformedBackendName" => Ok(Self::MalformedBackendName),
"BackendImportError" => Ok(Self::BackendImportError),
"InvalidHookName" => Ok(Self::InvalidHookName),
"InvalidAction" => Ok(Self::InvalidAction),
"UnsupportedHook" => Ok(Self::UnsupportedHook),
"MalformedHookArgument" => Ok(Self::MalformedHookArgument),
"HookRuntimeError" => Ok(Self::HookRuntimeError),
_ => Err(Error::DaemonError {
message: "Unknown error kind".into(),
}),
}
}
}
#[derive(Debug)]
pub(crate) struct Pep517Daemon {
script_path: PathBuf,
venv: Virtualenv,
source_tree: PathBuf,
stdout: Option<Lines<BufReader<ChildStdout>>>,
stdin: Option<ChildStdin>,
handle: Option<Child>,
last_response: Option<Pep517DaemonResponse>,
closed: bool,
}
impl Pep517Daemon {
pub(crate) async fn new(venv: &Virtualenv, source_tree: &Path) -> Result<Self, Error> {
// Write `hookd` to the virtual environment
let script_path = venv.bin_dir().join("hookd");
let mut file = File::create(&script_path).await?;
file.write_all(HOOKD_SOURCE.as_bytes()).await?;
Ok(Self {
script_path,
venv: venv.clone(),
source_tree: source_tree.to_path_buf(),
stdout: None,
stdin: None,
handle: None,
last_response: None,
closed: false,
})
}
async fn ensure_started(&mut self) -> Result<(), Error> {
let started = {
if let Some(handle) = self.handle.as_mut() {
// Check if the process has exited
handle.try_wait()?.is_none()
} else {
false
}
};
if !started {
let handle = self.start().await?;
self.handle = Some(handle);
self.closed = false;
let stdout = self
.handle
.as_mut()
.unwrap()
.stdout
.take()
.expect("stdout is available");
self.stdout = Some(tokio::io::AsyncBufReadExt::lines(BufReader::new(stdout)));
self.stdin = Some(
self.handle
.as_mut()
.unwrap()
.stdin
.take()
.expect("stdin is available"),
);
}
// Wait until ready
if self.receive_until_actionable().await? == Pep517DaemonResponse::Ready {
Ok(())
} else {
Err(Error::DaemonError {
message: "did not recieve ready".into(),
})
}
}
async fn start(&mut self) -> Result<Child, Error> {
let mut new_path = self.venv.bin_dir().into_os_string();
if let Some(path) = env::var_os("PATH") {
new_path.push(":");
new_path.push(path);
};
let handle = Command::new(self.venv.python_executable())
.args([self.script_path.clone()])
.current_dir(self.source_tree.clone())
// Activate the venv
.env("VIRTUAL_ENV", self.venv.root())
.env("PATH", new_path)
// Create pipes for communication
.stdin(Stdio::piped())
.stdout(Stdio::piped())
// Stderr doesn't have anything we need unless debugging
.stderr(Stdio::null())
.spawn()?;
debug!(
"Started new hook daemon in virtualenv {}",
self.venv.root().to_string_lossy()
);
Ok(handle)
}
async fn receive_one(&mut self) -> Result<Pep517DaemonResponse, Error> {
let stdout = self.stdout.as_mut().unwrap();
if let Some(line) = stdout.next_line().await? {
let response = Pep517DaemonResponse::from_line(line.as_str())?;
self.last_response = Some(response.clone());
Ok(response)
} else {
if let Some(output) = self.close().await? {
Err(Error::DaemonError {
message: format!(
"Daemon closed unexpectedly with code {:?}",
// TODO: debug output
output
),
})
} else {
Err(Error::DaemonError {
message: format!("Daemon closed unexpectedly"),
})
}
}
}
async fn receive_until_actionable(&mut self) -> Result<Pep517DaemonResponse, Error> {
loop {
let next = self.receive_one().await?;
match next {
Pep517DaemonResponse::Debug(message) => debug!("{message}"),
Pep517DaemonResponse::Expect(_) => continue,
Pep517DaemonResponse::Fatal(kind, message) => {
if let Pep517DaemonResponse::Traceback(traceback) = self.receive_one().await? {
error!("{}", traceback.replace("\\n", "\n").replace("\n\n", "\n"))
}
return Err(Error::DaemonError {
message: format!("Fatal error {kind}: {message}"),
});
}
_ => return Ok(next),
}
}
}
async fn run_hook(
&mut self,
backend: &Pep517Backend,
hook_name: &str,
mut args: Vec<&str>,
) -> Result<String, Error> {
self.ensure_started().await?;
let stdin = self.stdin.as_mut().unwrap();
// Always send run and the backend name
let mut commands = vec!["run", backend.backend.as_str()];
// Send backend paths
if let Some(backend_paths) = backend.backend_path.as_ref() {
for backend_path in backend_paths.iter() {
commands.push(backend_path)
}
}
commands.push("");
// Specify the hook
commands.push(hook_name);
// Consume the arguments
commands.append(&mut args);
// Send a trailing newline
commands.push("");
stdin.write_all(commands.join("\n").as_bytes()).await?;
stdin.flush().await?;
// Read the responses
loop {
let next = self.receive_until_actionable().await?;
match next {
Pep517DaemonResponse::Stderr(_) => continue,
Pep517DaemonResponse::Stdout(_) => continue,
Pep517DaemonResponse::Ok(result) => return Ok(result),
Pep517DaemonResponse::Error(_kind, message) => {
if let Pep517DaemonResponse::Traceback(message) =
self.receive_until_actionable().await?
{
error!("{}", message.replace("\\n", "\n").replace("\n\n", "\n"))
}
return Err(Error::DaemonError { message });
}
_ => break,
}
}
Err(Error::DaemonError {
message: format!("unexpected response {:?}", self.last_response).to_string(),
})
}
pub(crate) async fn prepare_metadata_for_build_wheel(
&mut self,
backend: &Pep517Backend,
metadata_directory: PathBuf,
) -> Result<Option<PathBuf>, Error> {
let result = self
.run_hook(
backend,
"prepare_metadata_for_build_wheel",
vec![metadata_directory.to_str().unwrap(), ""],
)
.await?;
Ok(Some(PathBuf::from_str(result.as_str()).unwrap()))
}
pub(crate) async fn get_requires_for_build(
&mut self,
backend: &Pep517Backend,
kind: BuildKind,
) -> Result<Vec<Requirement>, Error> {
let result = self
.run_hook(
backend,
format!("get_requires_for_build_{}", kind).as_str(),
vec![""],
)
.await?;
let requirements: Result<Vec<Requirement>, _> = result
.strip_prefix("[")
.unwrap()
.strip_suffix("]")
.unwrap()
.split(", ")
.map(|item| {
item.strip_prefix('\'')
.and_then(|item| item.strip_suffix('\''))
})
.filter(|item| item.is_some())
.map(|item| item.unwrap())
.filter(|item| !item.is_empty())
.map(|item| {
Requirement::from_str(item).map_err(|err| Error::DaemonError {
message: format!("Failed to parse {}: {}", item, err.to_string()),
})
})
.collect();
requirements
}
pub(crate) async fn build(
&mut self,
backend: &Pep517Backend,
kind: BuildKind,
wheel_directory: &Path,
metadata_directory: Option<&Path>,
) -> Result<String, Error> {
let result = self
.run_hook(
backend,
format!("build_{}", kind).as_str(),
vec![
wheel_directory.to_string_lossy().deref(),
"",
metadata_directory
.unwrap_or(Path::new(""))
.to_string_lossy()
.deref(),
],
)
.await?;
Ok(result)
}
pub(crate) async fn close(&mut self) -> Result<Option<Output>, Error> {
// Mark `closed` before attempting to close
// If there's an error on close, we should raise that instead of complaining it was never called
self.closed = true;
if let Some(mut handle) = self.handle.take() {
if handle.try_wait()?.is_none() {
// Send a shutdown command if it's not closed yet
let stdin = self.stdin.as_mut().unwrap();
stdin.write_all("shutdown\n".as_bytes()).await?;
}
Ok(Some(handle.wait_with_output().await?))
} else {
Ok(None)
}
}
}
impl Drop for Pep517Daemon {
fn drop(&mut self) {
if !self.closed {
panic!("`Pep517Daemon::close()` not called before drop.");
}
}
}

View File

@ -2,12 +2,10 @@
//! //!
//! <https://packaging.python.org/en/latest/specifications/source-distribution-format/> //! <https://packaging.python.org/en/latest/specifications/source-distribution-format/>
use std::env;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::io; use std::io;
use std::ops::Deref;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Output, Stdio}; use std::process::Output;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
@ -18,19 +16,19 @@ use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tempfile::{tempdir, tempdir_in, TempDir}; use tempfile::{tempdir, tempdir_in, TempDir};
use thiserror::Error; use thiserror::Error;
use tokio::fs::File; use tokio::process::Command;
use tokio::io::BufReader;
use tokio::io::{AsyncWriteExt, Lines};
use tokio::process::{Child, ChildStdin, ChildStdout, Command};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::{debug, error, info_span, instrument, Instrument}; use tracing::{debug, error, info_span, instrument, Instrument};
use crate::daemon::Pep517Daemon;
use distribution_types::Resolution; use distribution_types::Resolution;
use pep508_rs::Requirement; use pep508_rs::Requirement;
use puffin_extract::extract_source; use puffin_extract::extract_source;
use puffin_interpreter::{Interpreter, Virtualenv}; use puffin_interpreter::{Interpreter, Virtualenv};
use puffin_traits::{BuildContext, BuildKind, SetupPyStrategy, SourceBuildTrait}; use puffin_traits::{BuildContext, BuildKind, SetupPyStrategy, SourceBuildTrait};
mod daemon;
/// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory` /// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory`
static MISSING_HEADER_RE: Lazy<Regex> = Lazy::new(|| { static MISSING_HEADER_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new( Regex::new(
@ -44,8 +42,6 @@ static LD_NOT_FOUND_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"/usr/bin/ld: cannot find -l([a-zA-Z10-9]+): No such file or directory").unwrap() Regex::new(r"/usr/bin/ld: cannot find -l([a-zA-Z10-9]+): No such file or directory").unwrap()
}); });
static HOOKD_SOURCE: &'static str = include_str!("hookd.py");
/// The default backend to use when PEP 517 is used without a `build-system` section. /// The default backend to use when PEP 517 is used without a `build-system` section.
static DEFAULT_BACKEND: Lazy<Pep517Backend> = Lazy::new(|| Pep517Backend { static DEFAULT_BACKEND: Lazy<Pep517Backend> = Lazy::new(|| Pep517Backend {
backend: "setuptools.build_meta:__legacy__".to_string(), backend: "setuptools.build_meta:__legacy__".to_string(),
@ -200,388 +196,6 @@ struct Pep517Backend {
backend_path: Option<Vec<String>>, backend_path: Option<Vec<String>>,
} }
#[derive(Debug, Eq, PartialEq, Clone)]
enum Pep517DaemonResponse {
Debug(String),
Error(Pep517DaemonErrorKind, String),
Traceback(String),
Ok(String),
Stderr(PathBuf),
Stdout(PathBuf),
Expect(String),
Ready,
Fatal(String, String),
Shutdown,
}
impl Pep517DaemonResponse {
fn from_line(line: &str) -> Result<Self, Error> {
// Split on the first two spaces
let mut parts = line.splitn(3, ' ');
if let Some(kind) = parts.next() {
let response = match kind {
"DEBUG" => Self::Debug(parts.collect::<Vec<&str>>().join(" ")),
"EXPECT" => Self::Expect(parts.collect::<Vec<&str>>().join(" ")),
"OK" => Self::Ok(parts.collect::<Vec<&str>>().join(" ")),
"TRACEBACK" => Self::Traceback(parts.collect::<Vec<&str>>().join(" ")),
"ERROR" => Self::Error(
Pep517DaemonErrorKind::from_str(parts.next().unwrap())?,
parts.next().unwrap().to_string(),
),
"STDOUT" => Self::Stdout(parts.next().unwrap().into()),
"STDERR" => Self::Stderr(parts.next().unwrap().into()),
"READY" => Self::Ready,
"FATAL" => Self::Fatal(
parts.next().unwrap().to_string(),
parts.next().unwrap().to_string(),
),
"SHUTDOWN" => Self::Shutdown,
_ => {
return Err(Error::DaemonError {
message: format!("Unknown response: {}", line),
})
}
};
Ok(response)
} else {
Err(Error::DaemonError {
message: "No kind in response.".into(),
})
}
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
enum Pep517DaemonErrorKind {
MissingBackendModule,
MissingBackendAttribute,
MalformedBackendName,
BackendImportError,
InvalidHookName,
InvalidAction,
UnsupportedHook,
MalformedHookArgument,
HookRuntimeError,
}
impl Pep517DaemonErrorKind {
fn from_str(name: &str) -> Result<Self, Error> {
match name {
"MissingBackendModule" => Ok(Self::MissingBackendModule),
"MissingBackendAttribute" => Ok(Self::MissingBackendAttribute),
"MalformedBackendName" => Ok(Self::MalformedBackendName),
"BackendImportError" => Ok(Self::BackendImportError),
"InvalidHookName" => Ok(Self::InvalidHookName),
"InvalidAction" => Ok(Self::InvalidAction),
"UnsupportedHook" => Ok(Self::UnsupportedHook),
"MalformedHookArgument" => Ok(Self::MalformedHookArgument),
"HookRuntimeError" => Ok(Self::HookRuntimeError),
_ => Err(Error::DaemonError {
message: "Unknown error kind".into(),
}),
}
}
}
#[derive(Debug)]
struct Pep517Daemon {
script_path: PathBuf,
venv: Virtualenv,
source_tree: PathBuf,
stdout: Option<Lines<BufReader<ChildStdout>>>,
stdin: Option<ChildStdin>,
handle: Option<Child>,
last_response: Option<Pep517DaemonResponse>,
closed: bool,
}
impl Pep517Daemon {
async fn new(venv: &Virtualenv, source_tree: &Path) -> Result<Self, Error> {
// Write `hookd` to the virtual environment
let script_path = venv.bin_dir().join("hookd");
let mut file = File::create(&script_path).await?;
file.write_all(HOOKD_SOURCE.as_bytes()).await?;
Ok(Self {
script_path,
venv: venv.clone(),
source_tree: source_tree.to_path_buf(),
stdout: None,
stdin: None,
handle: None,
last_response: None,
closed: false,
})
}
async fn ensure_started(&mut self) -> Result<(), Error> {
let started = {
if let Some(handle) = self.handle.as_mut() {
// Check if the process has exited
handle.try_wait()?.is_none()
} else {
false
}
};
if !started {
let handle = self.start().await?;
self.handle = Some(handle);
self.closed = false;
let stdout = self
.handle
.as_mut()
.unwrap()
.stdout
.take()
.expect("stdout is available");
self.stdout = Some(tokio::io::AsyncBufReadExt::lines(BufReader::new(stdout)));
self.stdin = Some(
self.handle
.as_mut()
.unwrap()
.stdin
.take()
.expect("stdin is available"),
);
}
// Wait until ready
if self.receive_until_actionable().await? == Pep517DaemonResponse::Ready {
Ok(())
} else {
Err(Error::DaemonError {
message: "did not recieve ready".into(),
})
}
}
async fn start(&mut self) -> Result<Child, Error> {
let mut new_path = self.venv.bin_dir().into_os_string();
if let Some(path) = env::var_os("PATH") {
new_path.push(":");
new_path.push(path);
};
let handle = Command::new(self.venv.python_executable())
.args([self.script_path.clone()])
.current_dir(self.source_tree.clone())
// Activate the venv
.env("VIRTUAL_ENV", self.venv.root())
.env("PATH", new_path)
// Create pipes for communication
.stdin(Stdio::piped())
.stdout(Stdio::piped())
// Stderr doesn't have anything we need unless debugging
.stderr(Stdio::null())
.spawn()?;
debug!(
"Started new hook daemon in virtualenv {}",
self.venv.root().to_string_lossy()
);
Ok(handle)
}
async fn receive_one(&mut self) -> Result<Pep517DaemonResponse, Error> {
let mut stdout = self.stdout.as_mut().unwrap();
if let Some(line) = stdout.next_line().await? {
let response = Pep517DaemonResponse::from_line(line.as_str())?;
self.last_response = Some(response.clone());
Ok(response)
} else {
if let Some(output) = self.close().await? {
Err(Error::DaemonError {
message: format!(
"Daemon closed unexpectedly with code {:?}",
// TODO: debug output
output
),
})
} else {
Err(Error::DaemonError {
message: format!("Daemon closed unexpectedly"),
})
}
}
}
async fn receive_until_actionable(&mut self) -> Result<Pep517DaemonResponse, Error> {
loop {
let next = self.receive_one().await?;
match next {
Pep517DaemonResponse::Debug(message) => debug!("{message}"),
Pep517DaemonResponse::Expect(_) => continue,
Pep517DaemonResponse::Fatal(kind, message) => {
if let Pep517DaemonResponse::Traceback(traceback) = self.receive_one().await? {
error!("{}", traceback.replace("\\n", "\n").replace("\n\n", "\n"))
}
return Err(Error::DaemonError { message });
}
_ => return Ok(next),
}
}
}
async fn run_hook(
&mut self,
backend: &Pep517Backend,
hook_name: &str,
mut args: Vec<&str>,
) -> Result<String, Error> {
self.ensure_started().await?;
let stdin = self.stdin.as_mut().unwrap();
// Always send run and the backend name
let mut commands = vec!["run", backend.backend.as_str()];
// Send backend paths
if let Some(backend_paths) = backend.backend_path.as_ref() {
for backend_path in backend_paths.iter() {
commands.push(backend_path)
}
}
commands.push("");
// Specify the hook
commands.push(hook_name);
// Consume the arguments
commands.append(&mut args);
// Send a trailing newline
commands.push("");
stdin.write_all(commands.join("\n").as_bytes()).await?;
stdin.flush().await?;
// Read the responses
loop {
let next = self.receive_until_actionable().await?;
match next {
Pep517DaemonResponse::Stderr(_) => continue,
Pep517DaemonResponse::Stdout(_) => continue,
Pep517DaemonResponse::Ok(result) => return Ok(result),
Pep517DaemonResponse::Error(_kind, message) => {
if let Pep517DaemonResponse::Traceback(message) =
self.receive_until_actionable().await?
{
error!("{}", message.replace("\\n", "\n").replace("\n\n", "\n"))
}
return Err(Error::DaemonError { message });
}
_ => break,
}
}
Err(Error::DaemonError {
message: format!("unexpected response {:?}", self.last_response).to_string(),
})
}
async fn prepare_metadata_for_build_wheel(
&mut self,
backend: &Pep517Backend,
metadata_directory: PathBuf,
) -> Result<Option<PathBuf>, Error> {
let result = self
.run_hook(
backend,
"prepare_metadata_for_build_wheel",
vec![metadata_directory.to_str().unwrap(), ""],
)
.await?;
Ok(Some(PathBuf::from_str(result.as_str()).unwrap()))
}
async fn get_requires_for_build(
&mut self,
backend: &Pep517Backend,
kind: BuildKind,
) -> Result<Vec<Requirement>, Error> {
let result = self
.run_hook(
backend,
format!("get_requires_for_build_{}", kind).as_str(),
vec![""],
)
.await?;
let requirements: Result<Vec<Requirement>, _> = result
.strip_prefix("[")
.unwrap()
.strip_suffix("]")
.unwrap()
.split(", ")
.map(|item| {
item.strip_prefix('\'')
.and_then(|item| item.strip_suffix('\''))
})
.filter(|item| item.is_some())
.map(|item| item.unwrap())
.filter(|item| !item.is_empty())
.map(|item| {
Requirement::from_str(item).map_err(|err| Error::DaemonError {
message: format!("Failed to parse {}: {}", item, err.to_string()),
})
})
.collect();
requirements
}
async fn build(
&mut self,
backend: &Pep517Backend,
kind: BuildKind,
wheel_directory: &Path,
metadata_directory: Option<&Path>,
) -> Result<String, Error> {
let result = self
.run_hook(
backend,
format!("build_{}", kind).as_str(),
vec![
wheel_directory.to_string_lossy().deref(),
"",
metadata_directory
.unwrap_or(Path::new(""))
.to_string_lossy()
.deref(),
],
)
.await?;
Ok(result)
}
async fn close(&mut self) -> Result<Option<Output>, Error> {
// Mark `closed` before attempting to close
// If there's an error on close, we should raise that instead of complaining it was never called
self.closed = true;
if let Some(mut handle) = self.handle.take() {
if handle.try_wait()?.is_none() {
// Send a shutdown command if it's not closed yet
let stdin = self.stdin.as_mut().unwrap();
stdin.write_all("shutdown\n".as_bytes()).await?;
}
Ok(Some(handle.wait_with_output().await?))
} else {
Ok(None)
}
}
}
impl Drop for Pep517Daemon {
fn drop(&mut self) {
if !self.closed {
panic!("`Pep517Daemon::close()` not called before drop.");
}
}
}
/// Uses an [`Arc`] internally, clone freely /// Uses an [`Arc`] internally, clone freely
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct SourceBuildContext { pub struct SourceBuildContext {