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_preview::Preview;
use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment}; use uv_pypi_types::{Conflicts, ResolverMarkerEnvironment};
use uv_python::{PythonEnvironment, PythonInstallation}; use uv_python::{PythonEnvironment, PythonInstallation};
use uv_redacted::DisplaySafeUrl;
use uv_requirements::{ use uv_requirements::{
GroupsSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource, GroupsSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource,
RequirementsSpecification, SourceTree, SourceTreeResolver, 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. /// A summary of the changes made to the environment during an installation.

View File

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

View File

@ -19,7 +19,7 @@ use uv_configuration::{
use uv_dispatch::BuildDispatch; use uv_dispatch::BuildDispatch;
use uv_distribution::LoweredExtraBuildDependencies; use uv_distribution::LoweredExtraBuildDependencies;
use uv_distribution_types::{ 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_fs::{PortablePathBuf, Simplified};
use uv_installer::{InstallationStrategy, SitePackages}; use uv_installer::{InstallationStrategy, SitePackages};
@ -37,16 +37,16 @@ use uv_workspace::pyproject::Source;
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache}; use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache};
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; 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::resolution_markers;
use crate::commands::pip::{operations, resolution_tags}; use crate::commands::pip::{operations, resolution_tags};
use crate::commands::project::install_target::InstallTarget; use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::lock::{LockMode, LockOperation, LockResult}; use crate::commands::project::lock::{LockMode, LockOperation, LockResult};
use crate::commands::project::lock_target::LockTarget; use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{ use crate::commands::project::{
PlatformState, ProjectEnvironment, ProjectError, ScriptEnvironment, UniversalState, EnvironmentUpdate, PlatformState, ProjectEnvironment, ProjectError, ScriptEnvironment,
default_dependency_groups, detect_conflicts, script_extra_build_requires, script_specification, UniversalState, default_dependency_groups, detect_conflicts, script_extra_build_requires,
update_environment, script_specification, update_environment,
}; };
use crate::commands::{ExitStatus, diagnostics}; use crate::commands::{ExitStatus, diagnostics};
use crate::printer::Printer; use crate::printer::Printer;
@ -207,11 +207,12 @@ pub(crate) async fn sync(
}) })
.ok(); .ok();
let sync_report = SyncReport { let mut sync_report = SyncReport {
dry_run: dry_run.enabled(), dry_run: dry_run.enabled(),
environment: EnvironmentReport::from(&environment), environment: EnvironmentReport::from(&environment),
action: SyncAction::from(&environment), action: SyncAction::from(&environment),
target: TargetName::from(&target), target: TargetName::from(&target),
packages: Vec::new(),
}; };
// Show the intermediate results if relevant // Show the intermediate results if relevant
@ -292,7 +293,8 @@ pub(crate) async fn sync(
) )
.await .await
{ {
Ok(..) => { Ok(EnvironmentUpdate { changelog, .. }) => {
sync_report.packages = PackageChangeReport::from_changelog(&changelog);
// Generate a report for the script without a lockfile // Generate a report for the script without a lockfile
let report = Report { let report = Report {
schema: SchemaReport::default(), schema: SchemaReport::default(),
@ -387,27 +389,13 @@ pub(crate) async fn sync(
writeln!(printer.stderr(), "{message}")?; 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. // Identify the installation target.
let sync_target = identify_installation_target(&target, outcome.lock(), all_packages, &package); let sync_target = identify_installation_target(&target, outcome.lock(), all_packages, &package);
let state = state.fork(); let state = state.fork();
// Perform the sync operation. // Perform the sync operation.
match do_sync( let changelog = match do_sync(
sync_target, sync_target,
&environment, &environment,
&extras, &extras,
@ -430,13 +418,29 @@ pub(crate) async fn sync(
) )
.await .await
{ {
Ok(()) => {} Ok(changelog) => changelog,
Err(ProjectError::Operation(err)) => { Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls()) return diagnostics::OperationDiagnostic::native_tls(client_builder.is_native_tls())
.report(err) .report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into())); .map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
} }
Err(err) => return 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 { match outcome {
@ -614,7 +618,7 @@ pub(super) async fn do_sync(
dry_run: DryRun, dry_run: DryRun,
printer: Printer, printer: Printer,
preview: Preview, preview: Preview,
) -> Result<(), ProjectError> { ) -> Result<Changelog, ProjectError> {
// Extract the project settings. // Extract the project settings.
let InstallerSettingsRef { let InstallerSettingsRef {
index_locations, index_locations,
@ -825,7 +829,7 @@ pub(super) async fn do_sync(
let site_packages = SitePackages::from_environment(venv)?; let site_packages = SitePackages::from_environment(venv)?;
// Sync the environment. // Sync the environment.
operations::install( let changelog = operations::install(
&resolution, &resolution,
site_packages, site_packages,
InstallationStrategy::Strict, InstallationStrategy::Strict,
@ -850,7 +854,7 @@ pub(super) async fn do_sync(
) )
.await?; .await?;
Ok(()) Ok(changelog)
} }
/// Filter out any virtual workspace members. /// Filter out any virtual workspace members.
@ -1254,6 +1258,9 @@ struct SyncReport {
environment: EnvironmentReport, environment: EnvironmentReport,
/// The action performed during the sync, e.g., what was done to the environment. /// The action performed during the sync, e.g., what was done to the environment.
action: SyncAction, 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 // 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 // [`Report`] is intended to include these in user-facing output
@ -1276,6 +1283,7 @@ impl SyncReport {
let Self { let Self {
environment, environment,
action, action,
packages: _,
dry_run, dry_run,
target, target,
} = self; } = 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. /// The report for a lock operation.
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct LockReport { struct LockReport {

View File

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

View File

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