From 8ff74f6245416e01cf76f62471249b364bb685ad Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 9 Nov 2025 23:10:27 -0500 Subject: [PATCH 1/3] Add initial work --- crates/uv/src/commands/project/remove.rs | 2 +- crates/uv/src/commands/project/run.rs | 4 +- crates/uv/src/commands/project/sync.rs | 135 +++++++++++++++++----- crates/uv/src/commands/project/version.rs | 2 +- crates/uv/tests/it/sync.rs | 78 +++++++++++-- 5 files changed, 183 insertions(+), 38 deletions(-) diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 6487c5484843d..6b79cd7105f09 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -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) diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index e64fe62b5efb7..6f3d10e2281cd 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -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(), @@ -859,7 +859,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(), diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 6ee926daf01c7..20bed70b48ff0 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -19,7 +19,8 @@ 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, InstalledMetadata, LocalDist, Name, Requirement, + Resolution, ResolvedDist, SourceDist, }; use uv_fs::{PortablePathBuf, Simplified}; use uv_installer::{InstallationStrategy, SitePackages}; @@ -37,16 +38,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::{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 +208,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 @@ -283,7 +285,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(), @@ -378,27 +381,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, @@ -421,13 +410,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 { @@ -605,7 +610,7 @@ pub(super) async fn do_sync( dry_run: DryRun, printer: Printer, preview: Preview, -) -> Result<(), ProjectError> { +) -> Result { // Extract the project settings. let InstallerSettingsRef { index_locations, @@ -811,7 +816,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, @@ -836,7 +841,7 @@ pub(super) async fn do_sync( ) .await?; - Ok(()) + Ok(changelog) } /// Filter out any virtual workspace members. @@ -1240,6 +1245,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, // 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 @@ -1262,6 +1270,7 @@ impl SyncReport { let Self { environment, action, + packages: _, dry_run, target, } = self; @@ -1280,6 +1289,80 @@ 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. + version: String, + /// The source for URL-based requirements. + #[serde(skip_serializing_if = "Option::is_none")] + source: Option, + /// The action that was taken for the package. + action: PackageChangeAction, +} + +impl PackageChangeReport { + fn from_changelog(changelog: &Changelog) -> Vec { + 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: &LocalDist, action: PackageChangeAction) -> Self { + let installed_version = dist.installed_version(); + + Self { + name: dist.name().to_string(), + version: installed_version.version().to_string(), + source: installed_version + .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 { diff --git a/crates/uv/src/commands/project/version.rs b/crates/uv/src/commands/project/version.rs index 0f5441f2a573f..2805c1ce82762 100644 --- a/crates/uv/src/commands/project/version.rs +++ b/crates/uv/src/commands/project/version.rs @@ -668,7 +668,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) diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 46095a838a354..bd80cfb492820 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -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,8 @@ fn sync_dry_json() -> Result<()> { "implementation": "cpython" } }, - "action": "create" + "action": "create", + "packages": [] }, "lock": { "path": "[TEMP_DIR]/uv.lock", @@ -6688,7 +6699,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 @@ -6734,7 +6762,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 @@ -6793,7 +6838,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 From 50363d5a06ab8661543eef2cd421b22adfaeaf8a Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 9 Nov 2025 23:55:24 -0500 Subject: [PATCH 2/3] Inline `SyncReport` construction --- crates/uv/src/commands/project/sync.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 20bed70b48ff0..66478008db860 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -19,8 +19,8 @@ use uv_configuration::{ use uv_dispatch::BuildDispatch; use uv_distribution::LoweredExtraBuildDependencies; use uv_distribution_types::{ - DirectorySourceDist, Dist, Index, InstalledMetadata, LocalDist, Name, Requirement, - Resolution, ResolvedDist, SourceDist, + DirectorySourceDist, Dist, Index, InstalledMetadata, LocalDist, Name, Requirement, Resolution, + ResolvedDist, SourceDist, }; use uv_fs::{PortablePathBuf, Simplified}; use uv_installer::{InstallationStrategy, SitePackages}; @@ -286,14 +286,16 @@ pub(crate) async fn sync( .await { 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(), target: TargetName::from(&target), project: None, script: Some(ScriptReport::from(script)), - sync: sync_report, + sync: SyncReport { + packages: PackageChangeReport::from_changelog(&changelog), + ..sync_report + }, lock: None, dry_run: dry_run.enabled(), }; @@ -419,14 +421,15 @@ pub(crate) async fn sync( 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, + sync: SyncReport { + packages: PackageChangeReport::from_changelog(&changelog), + ..sync_report + }, lock: Some(lock_report), dry_run: dry_run.enabled(), }; From 675ea44b2711f8b6683f1421c6fd200a0a183460 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 9 Nov 2025 23:57:13 -0500 Subject: [PATCH 3/3] Remove superfluous `mut` --- crates/uv/src/commands/project/sync.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 66478008db860..a39872d451d4b 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -208,7 +208,7 @@ pub(crate) async fn sync( }) .ok(); - let mut sync_report = SyncReport { + let sync_report = SyncReport { dry_run: dry_run.enabled(), environment: EnvironmentReport::from(&environment), action: SyncAction::from(&environment),