use std::borrow::Cow;
use std::fmt::Write;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use owo_colors::OwoColorize;
use uv_cache::Cache;
use uv_fs::{CWD, Simplified, is_virtualenv_base, normalize_path};
use uv_preview::{Preview, PreviewFeature};
use uv_scripts::Pep723Metadata;
use uv_warnings::warn_user;
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache};
use crate::commands::ExitStatus;
use crate::printer::Printer;
pub(crate) async fn list(
project_dir: &Path,
paths: bool,
scripts: bool,
cache: &Cache,
workspace_cache: &WorkspaceCache,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
if scripts && !preview.is_enabled(PreviewFeature::WorkspaceListScripts) {
warn_user!(
"The `--scripts` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeature::WorkspaceListScripts
);
}
let workspace = Workspace::discover(
project_dir,
&DiscoveryOptions::default(),
cache,
workspace_cache,
)
.await?;
if scripts {
for script in find_scripts(workspace.install_path(), cache)? {
let script = script
.strip_prefix(workspace.install_path())
.context("PEP 723 script was discovered outside the workspace root")?;
writeln!(printer.stdout(), "{}", script.simplified_display().cyan())?;
}
return Ok(ExitStatus::Success);
}
for (name, member) in workspace.packages() {
if paths {
writeln!(
printer.stdout(),
"{}",
member.root().simplified_display().cyan()
)?;
} else {
writeln!(printer.stdout(), "{}", name.cyan())?;
}
}
Ok(ExitStatus::Success)
}
fn find_scripts(workspace_root: &Path, cache: &Cache) -> Result<Vec<PathBuf>> {
let cache_root = if cache.root().is_absolute() {
Cow::Borrowed(cache.root())
} else {
Cow::Owned(CWD.join(cache.root()))
};
let cache_root = normalize_path(cache_root);
let cache_is_nested =
cache_root.as_ref() != workspace_root && cache_root.starts_with(workspace_root);
let cache_root = cache_is_nested.then(|| cache_root.into_owned());
let mut builder = ignore::WalkBuilder::new(workspace_root);
ignore::WalkBuilder::hidden(&mut builder, false);
builder
.require_git(false)
.filter_entry(move |entry| {
let path = entry.path();
if cache_root
.as_ref()
.is_some_and(|cache_root| path.starts_with(cache_root))
{
return false;
}
if !entry
.file_type()
.is_some_and(|file_type| file_type.is_dir())
{
return true;
}
if entry.file_name() == ".git" {
return false;
}
if entry.file_name() == ".venv" {
return false;
}
!is_virtualenv_base(path)
});
let walker = builder.build();
let mut scripts = Vec::new();
for entry in walker {
let entry = entry.context("Failed to walk workspace while discovering PEP 723 scripts")?;
if !entry
.file_type()
.is_some_and(|file_type| file_type.is_file())
|| !is_python_script_path(entry.path())
{
continue;
}
let contents = fs_err::read(entry.path()).with_context(|| {
format!(
"Failed to read candidate PEP 723 script: {}",
entry.path().simplified_display()
)
})?;
if Pep723Metadata::parse(&contents)
.with_context(|| {
format!(
"Failed to parse PEP 723 script: {}",
entry.path().simplified_display()
)
})?
.is_some()
{
scripts.push(entry.into_path());
}
}
scripts.sort_unstable();
Ok(scripts)
}
fn is_python_script_path(path: &Path) -> bool {
path.extension().is_some_and(|extension| {
extension.to_str().is_some_and(|extension| {
extension.eq_ignore_ascii_case("py") || extension.eq_ignore_ascii_case("pyw")
})
})
}