Skip to main content

pray_core/
resolve_upstream_latest.rs

1use super::super::{source_map, ResolvedPackage, ResolvedProject};
2use super::lock::implied_upstream_source;
3use super::resolution_context::UpstreamResolutionContext;
4use crate::config::load_user_config;
5use crate::constraint::{latest_constraint_for_package, version_satisfies};
6use crate::lockfile::Lockfile;
7use crate::package_spec::parse_package_spec;
8use crate::package_spec_render::render_package_spec;
9use crate::paths::find_prayspec_file;
10use crate::resolve_context::ResolveOptions;
11use crate::resolve_git_sources::prepare_git_sources;
12use crate::{PrayError, PrayResult};
13use std::fs;
14use std::path::PathBuf;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct PathUpstreamLatestConstraint {
18    pub package_name: String,
19    pub upstream_name: String,
20    pub current_constraint: String,
21    pub latest_version: String,
22    pub new_constraint: String,
23    pub package_root: PathBuf,
24}
25
26pub fn plan_path_upstream_latest_constraints(
27    project: &ResolvedProject,
28    previous: Option<&Lockfile>,
29    selected: Option<&str>,
30    options: &ResolveOptions,
31) -> PrayResult<Vec<PathUpstreamLatestConstraint>> {
32    let user_config = load_user_config()?;
33    let git_sources = prepare_git_sources(
34        &project.project_root,
35        &project.manifest.sources,
36        previous,
37        options,
38    )?;
39    let sources = source_map(&project.manifest.sources);
40    let context = UpstreamResolutionContext::new(
41        &project.project_root,
42        &sources,
43        &git_sources,
44        &user_config,
45        previous,
46        options,
47    );
48    let mut plans = Vec::new();
49    for package in &project.packages {
50        if selected.is_some_and(|name| name != package.declaration.name) {
51            continue;
52        }
53        if let Some(plan) = plan_one_path_upstream(&context, package)? {
54            plans.push(plan);
55        }
56    }
57    Ok(plans)
58}
59
60pub fn apply_path_upstream_latest_constraints(
61    plans: &[PathUpstreamLatestConstraint],
62) -> PrayResult<()> {
63    for plan in plans {
64        let spec_path = find_prayspec_file(&plan.package_root)?;
65        let text = fs::read_to_string(&spec_path)?;
66        let mut spec = parse_package_spec(&text)?;
67        let Some(upstream) = spec.upstream.as_mut() else {
68            return Err(PrayError::Resolution(format!(
69                "package {} has no upstream pin",
70                plan.package_name
71            )));
72        };
73        upstream.constraint = plan.new_constraint.clone();
74        crate::transaction::write_file(&spec_path, render_package_spec(&spec))?;
75    }
76    Ok(())
77}
78
79fn plan_one_path_upstream(
80    context: &UpstreamResolutionContext<'_>,
81    package: &ResolvedPackage,
82) -> PrayResult<Option<PathUpstreamLatestConstraint>> {
83    let Some(upstream) = &package.spec.upstream else {
84        return Ok(None);
85    };
86    if package.declaration.path.is_none() {
87        return Ok(None);
88    }
89    let source = implied_upstream_source(&upstream.name, context.sources)?;
90    let resolved = context.resolve(&upstream.name, "*", source.as_deref())?;
91    if version_satisfies(&resolved.spec.version, &upstream.constraint)? {
92        return Ok(None);
93    }
94    let new_constraint =
95        latest_spec_upstream_constraint(&upstream.constraint, &resolved.spec.version)?;
96    if !version_satisfies(&resolved.spec.version, &new_constraint)? {
97        return Err(PrayError::Resolution(format!(
98            "derived constraint {new_constraint} does not admit latest {} for {}",
99            resolved.spec.version, package.declaration.name
100        )));
101    }
102    Ok(Some(PathUpstreamLatestConstraint {
103        package_name: package.declaration.name.clone(),
104        upstream_name: upstream.name.clone(),
105        current_constraint: upstream.constraint.clone(),
106        latest_version: resolved.spec.version,
107        new_constraint,
108        package_root: package.root.clone(),
109    }))
110}
111
112fn latest_spec_upstream_constraint(current: &str, latest_version: &str) -> PrayResult<String> {
113    let derived = latest_constraint_for_package(current, latest_version)?;
114    if derived.starts_with('=') {
115        return Ok(format!("= {latest_version}"));
116    }
117    Ok(derived)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::latest_spec_upstream_constraint;
123
124    #[test]
125    fn exact_pin_uses_spaced_equals() {
126        assert_eq!(
127            latest_spec_upstream_constraint("= 1.4.3", "1.4.4").expect("constraint"),
128            "= 1.4.4"
129        );
130    }
131
132    #[test]
133    fn pessimistic_pin_follows_latest_family() {
134        assert_eq!(
135            latest_spec_upstream_constraint("~> 1.4", "2.0.0").expect("constraint"),
136            "~> 2.0"
137        );
138    }
139}