use super::*;
use docker_api::{conn::TtyChunk, Docker};
use futures::{
future::ready,
stream::{iter, once, StreamExt},
};
use reqwest::Client;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
use tokio::{fs::File, io::AsyncWriteExt};
use tracing::info;
const DEFAULT_DOCKER_SOCKET: &str = "unix:///var/run/docker.sock";
#[cfg(feature = "options")]
pub(crate) mod options;
#[cfg(test)]
mod tests;
#[derive(Clone, Debug)]
pub struct DockerStrategy {
client: Client,
docker: Docker,
}
#[async_trait]
impl Strategy for DockerStrategy {
async fn builder_from_path(&self, path: &Path) -> Result<DynBuilder> {
Ok(Box::new(DockerBuilder::new(self.docker.clone(), path)))
}
async fn builder_from_crate(&self, _krate: &Path) -> Result<DynBuilder> {
todo!()
}
async fn builder_from_url(&self, url: &Url, _checksum: &[u8]) -> Result<DynBuilder> {
let dir = TempDir::new()?;
info!("Downloading crate from {url}");
let crate_file = self.client.get(url.as_str()).send().await?;
let mut stream = crate_file.bytes_stream();
let download_crate = dir.path().join("download.crate");
let download_folder = dir.path().join("output");
let mut file = File::create(&download_crate).await?;
while let Some(item) = stream.next().await {
file.write_all(&item?).await?;
}
file.flush().await?;
info!("Extracting crate");
let download_folder_clone = download_folder.clone();
tokio::spawn(async move {
use flate2::read::GzDecoder;
use tar::Archive;
let file = std::fs::File::open(&download_crate)?;
std::fs::create_dir(&download_folder)?;
let tar = GzDecoder::new(file);
let mut archive = Archive::new(tar);
archive.unpack(download_folder)?;
Ok(()) as Result<()>
})
.await??;
info!("Extracted crate");
let mut download_folder = tokio::fs::read_dir(&download_folder_clone).await?;
let download_folder = download_folder.next_entry().await?.unwrap().path();
let mut builder = DockerBuilder::new(self.docker.clone(), download_folder);
builder.tempdir = Some(dir);
Ok(Box::new(builder))
}
}
#[derive(Debug)]
pub struct DockerBuilder {
docker: Docker,
folder: PathBuf,
tempdir: Option<TempDir>,
}
impl DockerBuilder {
pub fn new<P: Into<PathBuf>>(docker: Docker, path: P) -> Self {
Self {
docker,
folder: path.into(),
tempdir: None,
}
}
pub fn docker(&self) -> &Docker {
&self.docker
}
pub fn folder(&self) -> &Path {
&self.folder
}
pub async fn delete(self) -> Result<()> {
tokio::fs::remove_dir_all(&self.folder).await?;
Ok(())
}
}
#[async_trait]
impl Builder for DockerBuilder {
async fn metadata(&self) -> Result<OutputStream<Metadata>> {
let containers = self.docker.containers();
let opts = docker_api::opts::ContainerCreateOpts::builder()
.attach_stdout(true)
.auto_remove(true)
.command(["cargo", "metadata", "--no-deps", "--format-version=1"])
.image("docker.io/library/rust")
.volumes([format!("{}:/crates:ro", self.folder.display())])
.working_dir("/crates")
.network_mode("none")
.build();
let container = containers.create(&opts).await?;
info!("Created docker container");
let output = container.attach().await?;
container.start().await?;
info!("Launched docker container");
let mut stderr = vec![];
let mut stdout = vec![];
let output = output
.map(Some)
.chain(once(ready(None)))
.flat_map(move |chunk| {
let items = match chunk {
None => {
let mut output = vec![];
if !stderr.is_empty() {
output.push(Ok(Output::Build(std::mem::take(&mut stderr))));
}
output.push(
serde_json::from_slice(&stdout)
.map(Output::Data)
.map_err(|err| err.into()),
);
output
}
Some(Err(error)) => vec![Err(error.into())],
Some(Ok(TtyChunk::StdErr(mut out))) => {
stderr.append(&mut out);
let mut lines: Vec<Vec<u8>> = stderr
.split_inclusive(|c| *c == b'\n')
.map(|slice| slice.to_vec())
.collect();
if let Some(last) = lines.last() {
if last.last() != Some(&b'\n') {
stderr = lines.pop().unwrap();
}
}
stderr.clear();
lines
.into_iter()
.map(|line| Ok(Output::Build(line)))
.collect()
}
Some(Ok(TtyChunk::StdOut(mut out))) => {
stdout.append(&mut out);
vec![]
}
Some(Ok(TtyChunk::StdIn(_))) => vec![],
};
iter(items)
})
.boxed();
Ok(output)
}
}