use std::io::Write;
#[cfg(all(test, unix))]
use std::path::Path;
use std::sync::{Arc, Mutex, PoisonError};
#[cfg(all(test, unix))]
use std::time::Duration;
use shep_core::paths::ShepPaths;
use crate::cli::{Format, InstallArgs};
use crate::dog_index::{self, Index};
use crate::exit::ExitCode;
use crate::fetch;
#[cfg(all(test, unix))]
use crate::fetch::RedirectPolicy;
use crate::output::Streams;
use crate::style::Presentation;
use super::consent::{Input, Prompt};
use super::dog_upgrade::{Goal, upgrade_dog};
#[cfg(all(test, unix))]
use super::install::{Lookup, Published};
use super::install::{Reach, install_dog};
use super::install_depends::Catalog;
use super::remove::{Removal, remove_dog};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Step {
Downloading,
Building,
Adopting,
}
#[derive(Clone)]
pub(crate) struct Progress(Arc<dyn Fn(Step) + Send + Sync>);
impl Progress {
pub(crate) fn new(sink: impl Fn(Step) + Send + Sync + 'static) -> Self {
Self(Arc::new(sink))
}
pub(super) fn tell(&self, step: Step) {
(self.0)(step);
}
}
impl core::fmt::Debug for Progress {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Progress").finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Job {
Install {
adopt_as: String,
},
Upgrade {
name: String,
},
Delete {
name: String,
},
}
impl Job {
pub(crate) fn name(&self) -> &str {
match self {
Self::Install { adopt_as } => adopt_as,
Self::Upgrade { name } | Self::Delete { name } => name,
}
}
}
pub(crate) async fn run(
paths: &ShepPaths,
job: Job,
reach: Reach,
progress: Progress,
answer: impl FnMut(&Prompt) -> bool + Send + 'static,
) -> (ExitCode, String) {
let reach = Reach {
progress: Some(progress),
..reach
};
run_with(paths, &dog_index::index_url(), job, reach, answer).await
}
#[cfg(all(test, unix))]
pub(crate) fn stub_reach(cargo: &Path) -> Reach {
Reach {
cargo: Some(cargo.to_path_buf()),
lookup: Lookup::Canned(Published::Unreported),
assets: RedirectPolicy::loopback("127.0.0.1", &["localhost"]),
lock_wait: Duration::from_millis(100),
progress: None,
}
}
pub(super) async fn run_with(
paths: &ShepPaths,
url: &str,
job: Job,
reach: Reach,
answer: impl FnMut(&Prompt) -> bool + Send + 'static,
) -> (ExitCode, String) {
let capture = Capture::default();
let (mut out, mut err) = (capture.clone(), capture.clone());
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: Presentation::BARE,
fmt: Format::Table,
};
let mut input = Input::answering(answer);
let code = dispatch(&mut streams, paths, url, job, &reach, &mut input).await;
(code, capture.text())
}
async fn dispatch(
streams: &mut Streams<'_>,
paths: &ShepPaths,
url: &str,
job: Job,
reach: &Reach,
input: &mut Input,
) -> ExitCode {
match job {
Job::Delete { name } => remove_dog(streams, paths, &name, Removal::Delete).await,
Job::Install { adopt_as } => {
let index = match cached(streams, paths, url) {
Ok(index) => index,
Err(code) => return code,
};
let Some(dog) = index.dogs.iter().find(|dog| dog.adopt_as == adopt_as) else {
return streams.fail(
ExitCode::NotFound,
&format!("the dog index has no dog called `{adopt_as}`"),
);
};
let args = InstallArgs {
dog: Some(adopt_as),
yes: false,
no_enable: false,
refresh: false,
cargo: false,
git_ref: None,
};
let catalog = Catalog::new(url, &index.dogs);
install_dog(streams, paths, &args, reach, input, dog, &catalog).await
}
Job::Upgrade { name } => {
let index = match cached(streams, paths, url) {
Ok(index) => index,
Err(code) => return code,
};
let catalog = Catalog::new(url, &index.dogs);
upgrade_dog(streams, paths, reach, input, &catalog, &name, &Goal::Follow).await
}
}
}
fn cached(streams: &mut Streams<'_>, paths: &ShepPaths, url: &str) -> Result<Index, ExitCode> {
dog_index::cached_only(paths, url).ok_or_else(|| {
streams.fail(
ExitCode::Failure,
&format!(
"reading the dog index from {}: no cached copy; `shep install` fetches it",
fetch::url_for_message(url)
),
)
})
}
#[derive(Clone, Default)]
pub(super) struct Capture(Arc<Mutex<Vec<u8>>>);
impl Capture {
pub(super) fn text(&self) -> String {
let bytes = self.0.lock().unwrap_or_else(PoisonError::into_inner);
String::from_utf8_lossy(&bytes).into_owned()
}
}
impl Write for Capture {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0
.lock()
.unwrap_or_else(PoisonError::into_inner)
.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}