use std::path::Path;
use std::process::Stdio;
use std::str::FromStr;
use std::time::Duration;
use anyhow::{Context, Result};
use tokio::io::AsyncWriteExt;
use tokio::process::{ChildStdin, Command};
use tracing::debug;
use uv_bin_install::{BinVersion, Binary, ResolvedVersion, bin_install, find_matching_version};
use uv_cache::Cache;
use uv_client::BaseClientBuilder;
use crate::child::run_to_completion;
use crate::commands::ExitStatus;
use crate::commands::reporters::BinaryDownloadReporter;
use crate::printer::Printer;
const WORKSPACE_METADATA_WRITE_TIMEOUT: Duration = Duration::from_mins(1);
async fn write_workspace_metadata(mut stdin: ChildStdin, workspace_metadata: String) -> Result<()> {
match tokio::time::timeout(
WORKSPACE_METADATA_WRITE_TIMEOUT,
stdin.write_all(workspace_metadata.as_bytes()),
)
.await
{
Err(err) => Err(err).context("Timed out while writing workspace metadata to `ty check`"),
Ok(Err(err)) if err.kind() == std::io::ErrorKind::BrokenPipe => Ok(()),
Ok(Err(err)) => Err(err).context("Failed to write workspace metadata to `ty check`"),
Ok(Ok(())) => Ok(()),
}
}
pub(super) async fn run(
version: Option<String>,
target_dir: &Path,
venv_path: Option<&Path>,
workspace_metadata: Option<String>,
exclude_newer: Option<jiff::Timestamp>,
client_builder: &BaseClientBuilder<'_>,
cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
let retry_policy = client_builder.retry_policy();
let ty_client = client_builder.clone().retries(0).build()?;
let reporter = BinaryDownloadReporter::single(printer);
let bin_version = version
.as_deref()
.map(BinVersion::from_str)
.transpose()?
.unwrap_or(BinVersion::Default);
let resolved = match bin_version {
BinVersion::Default => {
let constraints = Binary::Ty.default_constraints();
let resolved = find_matching_version(
Binary::Ty,
Some(&constraints),
exclude_newer,
&ty_client,
&retry_policy,
)
.await
.with_context(|| {
format!("Failed to find ty version matching default constraints: {constraints}")
})?;
debug!("Resolved `ty@{constraints}` to `ty=={}`", resolved.version);
resolved
}
BinVersion::Pinned(version) => {
if exclude_newer.is_some() {
debug!("`--exclude-newer` is ignored for pinned version `{version}`");
}
let resolved = ResolvedVersion::from_version(Binary::Ty, version)?;
debug!("Using `ty=={}`", resolved.version);
resolved
}
BinVersion::Latest => {
let resolved =
find_matching_version(Binary::Ty, None, exclude_newer, &ty_client, &retry_policy)
.await
.with_context(|| "Failed to find latest ty version")?;
debug!("Resolved `ty@latest` to `ty=={}`", resolved.version);
resolved
}
BinVersion::Constraint(constraints) => {
let resolved = find_matching_version(
Binary::Ty,
Some(&constraints),
exclude_newer,
&ty_client,
&retry_policy,
)
.await
.with_context(|| format!("Failed to find ty version matching: {constraints}"))?;
debug!("Resolved `ty@{constraints}` to `ty=={}`", resolved.version);
resolved
}
};
let ty_path = bin_install(
Binary::Ty,
&resolved,
&ty_client,
&retry_policy,
cache,
&reporter,
)
.await
.with_context(|| format!("Failed to install ty {}", resolved.version))?;
let mut command = Command::new(&ty_path);
command.current_dir(target_dir);
command.arg("check");
command.env("TY_UV", "1");
if let Some(venv_path) = venv_path {
command.env("VIRTUAL_ENV", venv_path);
}
if workspace_metadata.is_some() {
command.env("TY_UV_METADATA", "1");
command.stdin(Stdio::piped());
command.kill_on_drop(true);
} else {
command.env_remove("TY_UV_METADATA");
}
let mut handle = command.spawn().context("Failed to spawn `ty check`")?;
let writer = if let Some(workspace_metadata) = workspace_metadata {
debug!("Passing workspace metadata to `ty check` via stdin");
let stdin = handle
.stdin
.take()
.context("Failed to open stdin for `ty check`")?;
Some(write_workspace_metadata(stdin, workspace_metadata))
} else {
None
};
if let Some(writer) = writer {
let (status, ()) = tokio::try_join!(run_to_completion(handle), writer)?;
Ok(status)
} else {
run_to_completion(handle).await
}
}