Add initial work

This commit is contained in:
Liam 2025-11-09 23:10:27 -05:00 committed by Tomasz (Tom) Kramkowski
parent 9f51dca37c
commit fc5d5e871c
6 changed files with 200 additions and 38 deletions

View File

@ -34,6 +34,7 @@ use uv_platform_tags::Tags;
use uv_preview::Preview;
use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment};
use uv_python::{PythonEnvironment, PythonInstallation};
use uv_redacted::DisplaySafeUrl;
use uv_requirements::{
GroupsSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource,
RequirementsSpecification, SourceTree, SourceTreeResolver,
@ -454,6 +455,20 @@ impl ChangedDist {
},
}
}
pub(crate) fn version(&self) -> Option<&Version> {
match self {
Self::Local(dist) => Some(dist.installed_version().version()),
Self::Remote(dist) => dist.version(),
}
}
pub(crate) fn url(&self) -> Option<&DisplaySafeUrl> {
match self {
Self::Local(dist) => dist.installed_version().url(),
Self::Remote(dist) => dist.version_or_url().url().map(|url| &**url),
}
}
}
/// A summary of the changes made to the environment during an installation.

View File

@ -375,7 +375,7 @@ pub(crate) async fn remove(
)
.await
{
Ok(()) => {}
Ok(_) => {}
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)

View File

@ -339,7 +339,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
)
.await
{
Ok(()) => {}
Ok(_) => {}
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),
@ -867,7 +867,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
)
.await
{
Ok(()) => {}
Ok(_) => {}
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
client_builder.is_native_tls(),

View File

@ -19,7 +19,7 @@ use uv_configuration::{
use uv_dispatch::BuildDispatch;
use uv_distribution::LoweredExtraBuildDependencies;
use uv_distribution_types::{
DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist,
DirectorySourceDist, Dist, Index, Name, Requirement, Resolution, ResolvedDist, SourceDist,
};
use uv_fs::{PortablePathBuf, Simplified};
use uv_installer::{InstallationStrategy, SitePackages};
@ -37,16 +37,16 @@ use uv_workspace::pyproject::Source;
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache};
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
use crate::commands::pip::operations::Modifications;
use crate::commands::pip::operations::{ChangedDist, Changelog, Modifications};
use crate::commands::pip::resolution_markers;
use crate::commands::pip::{operations, resolution_tags};
use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::lock::{LockMode, LockOperation, LockResult};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
PlatformState, ProjectEnvironment, ProjectError, ScriptEnvironment, UniversalState,
default_dependency_groups, detect_conflicts, script_extra_build_requires, script_specification,
update_environment,
EnvironmentUpdate, PlatformState, ProjectEnvironment, ProjectError, ScriptEnvironment,
UniversalState, default_dependency_groups, detect_conflicts, script_extra_build_requires,
script_specification, update_environment,
};
use crate::commands::{ExitStatus, diagnostics};
use crate::printer::Printer;
@ -207,11 +207,12 @@ pub(crate) async fn sync(
})
.ok();
let sync_report = SyncReport {
let mut sync_report = SyncReport {
dry_run: dry_run.enabled(),
environment: EnvironmentReport::from(&environment),
action: SyncAction::from(&environment),
target: TargetName::from(&target),
packages: Vec::new(),
};
// Show the intermediate results if relevant
@ -292,7 +293,8 @@ pub(crate) async fn sync(
)
.await
{
Ok(..) => {
Ok(EnvironmentUpdate { changelog, .. }) => {
sync_report.packages = PackageChangeReport::from_changelog(&changelog);
// Generate a report for the script without a lockfile
let report = Report {
schema: SchemaReport::default(),
@ -387,27 +389,13 @@ pub(crate) async fn sync(
writeln!(printer.stderr(), "{message}")?;
}
let report = Report {
schema: SchemaReport::default(),
target: TargetName::from(&target),
project: target.project().map(ProjectReport::from),
script: target.script().map(ScriptReport::from),
sync: sync_report,
lock: Some(lock_report),
dry_run: dry_run.enabled(),
};
if let Some(output) = report.format(output_format) {
writeln!(printer.stdout_important(), "{output}")?;
}
// Identify the installation target.
let sync_target = identify_installation_target(&target, outcome.lock(), all_packages, &package);
let state = state.fork();
// Perform the sync operation.
match do_sync(
let changelog = match do_sync(
sync_target,
&environment,
&extras,
@ -430,13 +418,29 @@ pub(crate) async fn sync(
)
.await
{
Ok(()) => {}
Ok(changelog) => changelog,
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
sync_report.packages = PackageChangeReport::from_changelog(&changelog);
let report = Report {
schema: SchemaReport::default(),
target: TargetName::from(&target),
project: target.project().map(ProjectReport::from),
script: target.script().map(ScriptReport::from),
sync: sync_report,
lock: Some(lock_report),
dry_run: dry_run.enabled(),
};
if let Some(output) = report.format(output_format) {
writeln!(printer.stdout_important(), "{output}")?;
}
match outcome {
@ -614,7 +618,7 @@ pub(super) async fn do_sync(
dry_run: DryRun,
printer: Printer,
preview: Preview,
) -> Result<(), ProjectError> {
) -> Result<Changelog, ProjectError> {
// Extract the project settings.
let InstallerSettingsRef {
index_locations,
@ -825,7 +829,7 @@ pub(super) async fn do_sync(
let site_packages = SitePackages::from_environment(venv)?;
// Sync the environment.
operations::install(
let changelog = operations::install(
&resolution,
site_packages,
InstallationStrategy::Strict,
@ -850,7 +854,7 @@ pub(super) async fn do_sync(
)
.await?;
Ok(())
Ok(changelog)
}
/// Filter out any virtual workspace members.
@ -1254,6 +1258,9 @@ struct SyncReport {
environment: EnvironmentReport,
/// The action performed during the sync, e.g., what was done to the environment.
action: SyncAction,
/// The packages that changed during the sync.
#[serde(default)]
packages: Vec<PackageChangeReport>,
// We store these fields so the report can format itself self-contained, but the outer
// [`Report`] is intended to include these in user-facing output
@ -1276,6 +1283,7 @@ impl SyncReport {
let Self {
environment,
action,
packages: _,
dry_run,
target,
} = self;
@ -1294,6 +1302,77 @@ impl SyncReport {
}
}
/// A summary of a single package change performed during sync.
#[derive(Serialize, Debug, Clone)]
struct PackageChangeReport {
/// The normalized package name.
name: String,
/// The resolved version of the package.
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<uv_pep440::Version>,
/// The source for URL-based requirements.
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<PackageChangeSourceReport>,
/// The action that was taken for the package.
action: PackageChangeAction,
}
impl PackageChangeReport {
fn from_changelog(changelog: &Changelog) -> Vec<Self> {
let mut changes: Vec<_> = changelog
.uninstalled
.iter()
.map(|dist| Self::from_dist(dist, PackageChangeAction::Removed))
.chain(
changelog
.installed
.iter()
.map(|dist| Self::from_dist(dist, PackageChangeAction::Added)),
)
.chain(
changelog
.reinstalled
.iter()
.map(|dist| Self::from_dist(dist, PackageChangeAction::Reinstalled)),
)
.collect();
changes.sort_by(|a, b| {
a.name
.cmp(&b.name)
.then_with(|| a.action.cmp(&b.action))
.then_with(|| a.version.cmp(&b.version))
});
changes
}
fn from_dist(dist: &ChangedDist, action: PackageChangeAction) -> Self {
Self {
name: dist.name().to_string(),
version: dist.version().cloned(),
source: dist.url().map(|url| PackageChangeSourceReport {
url: url.to_string(),
}),
action,
}
}
}
/// The action taken on an individual package during sync.
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
enum PackageChangeAction {
Removed,
Added,
Reinstalled,
}
/// The source for a package change, when it originated from a URL requirement.
#[derive(Serialize, Debug, Clone)]
struct PackageChangeSourceReport {
url: String,
}
/// The report for a lock operation.
#[derive(Debug, Serialize)]
struct LockReport {

View File

@ -680,7 +680,7 @@ async fn lock_and_sync(
)
.await
{
Ok(()) => {}
Ok(_) => {}
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err)

View File

@ -420,7 +420,14 @@ fn sync_json() -> Result<()> {
"implementation": "cpython"
}
},
"action": "check"
"action": "check",
"packages": [
{
"name": "iniconfig",
"version": "2.0.0",
"action": "added"
}
]
},
"lock": {
"path": "[TEMP_DIR]/uv.lock",
@ -464,7 +471,8 @@ fn sync_json() -> Result<()> {
"implementation": "cpython"
}
},
"action": "check"
"action": "check",
"packages": []
},
"lock": {
"path": "[TEMP_DIR]/uv.lock",
@ -503,7 +511,8 @@ fn sync_json() -> Result<()> {
"implementation": "cpython"
}
},
"action": "check"
"action": "check",
"packages": []
},
"lock": {
"path": "[TEMP_DIR]/uv.lock",
@ -569,7 +578,8 @@ fn sync_json() -> Result<()> {
"implementation": "cpython"
}
},
"action": "check"
"action": "check",
"packages": []
},
"lock": {
"path": "[TEMP_DIR]/uv.lock",
@ -629,7 +639,14 @@ fn sync_dry_json() -> Result<()> {
"implementation": "cpython"
}
},
"action": "create"
"action": "create",
"packages": [
{
"name": "iniconfig",
"version": "2.0.0",
"action": "added"
}
]
},
"lock": {
"path": "[TEMP_DIR]/uv.lock",
@ -6856,7 +6873,24 @@ fn sync_active_script_environment_json() -> Result<()> {
"implementation": "cpython"
}
},
"action": "create"
"action": "create",
"packages": [
{
"name": "anyio",
"version": "4.3.0",
"action": "added"
},
{
"name": "idna",
"version": "3.6",
"action": "added"
},
{
"name": "sniffio",
"version": "1.3.1",
"action": "added"
}
]
},
"lock": null,
"dry_run": false
@ -6902,7 +6936,24 @@ fn sync_active_script_environment_json() -> Result<()> {
"implementation": "cpython"
}
},
"action": "create"
"action": "create",
"packages": [
{
"name": "anyio",
"version": "4.3.0",
"action": "added"
},
{
"name": "idna",
"version": "3.6",
"action": "added"
},
{
"name": "sniffio",
"version": "1.3.1",
"action": "added"
}
]
},
"lock": null,
"dry_run": false
@ -6961,7 +7012,24 @@ fn sync_active_script_environment_json() -> Result<()> {
"implementation": "cpython"
}
},
"action": "update"
"action": "update",
"packages": [
{
"name": "anyio",
"version": "4.3.0",
"action": "added"
},
{
"name": "idna",
"version": "3.6",
"action": "added"
},
{
"name": "sniffio",
"version": "1.3.1",
"action": "added"
}
]
},
"lock": null,
"dry_run": false