Skip to main content

pray_core/
resolve.rs

1use crate::constraint::version_satisfies;
2use crate::lockfile::Lockfile;
3use crate::manifest::{Manifest, ManifestPackage, ManifestSource};
4use crate::package_spec::{parse_package_spec, PackageSpec};
5use crate::resolve_context::ResolveOptions;
6use crate::resolve_exports::{
7    build_skill_file_index, load_export_bodies, load_package_file_bytes, read_text, select_exports,
8};
9use crate::resolve_git_sources::{
10    prepare_git_sources, prepare_pray_ssh_host_keys, GitSourceCheckout,
11};
12
13use crate::paths::find_prayspec_file;
14pub use crate::resolve_git::{discover_distribution_root, git_source_cache_directory};
15pub use crate::resolve_git_refresh::{
16    annotate_failed_git_refresh, annotate_missing_git_catalog,
17    resolution_may_benefit_from_git_source_refresh,
18};
19pub use crate::resolve_git_sources::refresh_git_sources;
20use crate::{PrayError, PrayResult};
21use std::collections::BTreeMap;
22use std::fs;
23use std::path::{Path, PathBuf};
24
25#[derive(Debug, Clone)]
26pub struct ResolvedProject {
27    pub manifest_path: PathBuf,
28    pub project_root: PathBuf,
29    pub manifest: Manifest,
30    pub manifest_hash: String,
31    pub packages: Vec<ResolvedPackage>,
32    pub local_files: Vec<ResolvedLocalFile>,
33    pub source_revisions: BTreeMap<String, String>,
34    pub source_host_keys: BTreeMap<String, String>,
35    pub environment: Option<String>,
36}
37
38#[derive(Debug, Clone)]
39pub struct ResolvedPackage {
40    pub declaration: ManifestPackage,
41    pub root: PathBuf,
42    pub spec: PackageSpec,
43    pub tree_hash: String,
44    pub artifact_hash: String,
45    pub artifact: String,
46    pub selected_exports: Vec<String>,
47    pub source_checksum: String,
48    pub export_bodies: BTreeMap<String, String>,
49    pub skill_files: BTreeMap<String, Vec<String>>,
50    pub signer_fingerprint: Option<String>,
51    /// Highest non-yanked version in registry metadata when the package came from a registry source.
52    pub registry_latest_version: Option<String>,
53    /// True when the package was declared in Prayfile; false for transitive dependencies.
54    pub explicit: bool,
55    pub upstream: Option<crate::package_upstream::LockedUpstream>,
56}
57
58#[derive(Debug, Clone)]
59pub struct ResolvedLocalFile {
60    pub path: PathBuf,
61    pub manifest_path: String,
62    pub content: String,
63    pub position: String,
64    pub optional: bool,
65}
66
67impl ResolvedProject {
68    pub fn lockfile_hash(&self) -> PrayResult<String> {
69        Ok(self.manifest_hash.clone())
70    }
71}
72
73pub fn project_root_from_manifest(manifest_path: &Path) -> PathBuf {
74    match manifest_path.parent() {
75        Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
76        _ => PathBuf::from("."),
77    }
78}
79
80fn canonical_project_root(manifest_path: &Path) -> PrayResult<PathBuf> {
81    let root = project_root_from_manifest(manifest_path);
82    if root.is_absolute() {
83        return Ok(root);
84    }
85    let cwd = std::env::current_dir().map_err(|error| {
86        PrayError::Resolution(format!("failed to resolve project root from cwd: {error}"))
87    })?;
88    Ok(cwd.join(root))
89}
90
91pub fn resolve_project(manifest_path: &Path) -> PrayResult<ResolvedProject> {
92    resolve_project_with_options(manifest_path, &ResolveOptions::default())
93}
94
95pub fn resolve_project_with_git_refresh_fallback(
96    manifest_path: &Path,
97    options: &ResolveOptions,
98    allow_git_refresh_fallback: bool,
99) -> PrayResult<ResolvedProject> {
100    match resolve_project_with_options(manifest_path, options) {
101        Ok(project) => Ok(project),
102        Err(PrayError::Resolution(message))
103            if allow_git_refresh_fallback
104                && !options.offline
105                && !options.refresh_source_revisions
106                && resolution_may_benefit_from_git_source_refresh(&message) =>
107        {
108            let refreshed_options = ResolveOptions {
109                refresh_source_revisions: true,
110                ..options.clone()
111            };
112            match resolve_project_with_options(manifest_path, &refreshed_options) {
113                Ok(project) => Ok(project),
114                Err(error) => {
115                    let lockfile_path =
116                        project_root_from_manifest(manifest_path).join("Prayfile.lock");
117                    Err(annotate_failed_git_refresh(&lockfile_path, error))
118                }
119            }
120        }
121        Err(error) => Err(error),
122    }
123}
124
125pub fn resolve_project_with_options(
126    manifest_path: &Path,
127    options: &ResolveOptions,
128) -> PrayResult<ResolvedProject> {
129    let project_root = canonical_project_root(manifest_path)?;
130    resolve_project_in_context(manifest_path, &project_root, options)
131}
132
133#[path = "resolve_project.rs"]
134mod project;
135pub use project::{resolve_manifest_in_context, resolve_project_in_context};
136
137#[path = "resolve_package_root.rs"]
138mod package_root;
139pub(crate) use package_root::{implied_source_name, resolve_package_root, PackageRootResolution};
140
141#[path = "resolve_upstream.rs"]
142pub mod resolve_upstream;
143pub use resolve_upstream::apply_path_upstream_refreshes;
144
145fn resolve_package(
146    project_root: &Path,
147    sources: &BTreeMap<String, ManifestSource>,
148    git_sources: &BTreeMap<String, GitSourceCheckout>,
149    user_config: &crate::config::PrayConfig,
150    declaration: &ManifestPackage,
151    lockfile: Option<&Lockfile>,
152    options: &ResolveOptions,
153) -> PrayResult<ResolvedPackage> {
154    let PackageRootResolution {
155        root,
156        signer_fingerprint,
157        registry_latest_version,
158    } = resolve_package_root(
159        project_root,
160        sources,
161        git_sources,
162        user_config,
163        declaration,
164        lockfile,
165        options,
166    )?;
167    let spec_path = find_prayspec_file(&root)?;
168    let spec_text = fs::read_to_string(&spec_path)?;
169    let spec = parse_package_spec(&spec_text)?.canonicalized();
170    if spec.name != declaration.name {
171        return Err(PrayError::Resolution(format!(
172            "package path {:?} declares {:?}, expected {:?}",
173            root, spec.name, declaration.name
174        )));
175    }
176    if !version_satisfies(&spec.version, &declaration.constraint)? {
177        return Err(PrayError::Resolution(format!(
178            "package {} version {} does not satisfy constraint {}",
179            declaration.name, spec.version, declaration.constraint
180        )));
181    }
182    let selected_exports = select_exports(declaration, &spec)?;
183    let file_bytes = load_package_file_bytes(&root, &spec)?;
184    let tree_hash = PackageSpec::tree_hash_from_file_bytes(&file_bytes)?;
185    let export_bodies = load_export_bodies(&file_bytes, &spec, &selected_exports)?;
186    let skill_files = build_skill_file_index(&spec);
187    let source_checksum = tree_hash.clone();
188    let upstream_context = resolve_upstream::UpstreamResolutionContext::new(
189        project_root,
190        sources,
191        git_sources,
192        user_config,
193        lockfile,
194        options,
195    );
196    let upstream = resolve_upstream::lock_path_upstream(&upstream_context, declaration, &spec)?;
197    Ok(ResolvedPackage {
198        declaration: declaration.clone(),
199        root,
200        spec: spec.clone(),
201        tree_hash: tree_hash.clone(),
202        artifact_hash: tree_hash.clone(),
203        artifact: format!(
204            "path:{}",
205            spec_path.parent().unwrap_or(&spec_path).to_string_lossy()
206        ),
207        selected_exports,
208        source_checksum,
209        export_bodies,
210        skill_files,
211        signer_fingerprint,
212        registry_latest_version,
213        explicit: false,
214        upstream,
215    })
216}
217
218pub fn missing_local_embed_guidance(path: impl AsRef<str>) -> String {
219    let path = path.as_ref();
220    format!(
221        "Prayfile lists `local \"{path}\"` but the file does not exist. \
222         Create the file or remove the entry from Prayfile, then run `pray install`."
223    )
224}
225
226fn resolve_local_file(
227    project_root: &Path,
228    declaration: &crate::manifest::ManifestLocal,
229) -> PrayResult<ResolvedLocalFile> {
230    let path = project_root.join(&declaration.path);
231    if !path.exists() {
232        if declaration.optional {
233            return Ok(ResolvedLocalFile {
234                path,
235                manifest_path: declaration.path.clone(),
236                content: String::new(),
237                position: declaration.position.clone(),
238                optional: true,
239            });
240        }
241        return Err(PrayError::Resolution(missing_local_embed_guidance(
242            &declaration.path,
243        )));
244    }
245    Ok(ResolvedLocalFile {
246        content: read_text(&path)?,
247        path,
248        manifest_path: declaration.path.clone(),
249        position: declaration.position.clone(),
250        optional: declaration.optional,
251    })
252}
253
254fn source_map(sources: &[ManifestSource]) -> BTreeMap<String, ManifestSource> {
255    sources
256        .iter()
257        .map(|source| (source.name.clone(), source.clone()))
258        .collect()
259}
260
261#[cfg(test)]
262mod tests {
263    use super::{discover_distribution_root, project_root_from_manifest};
264    use std::fs;
265    use std::path::Path;
266
267    #[test]
268    fn project_root_from_manifest_uses_cwd_for_bare_filename() {
269        let root = project_root_from_manifest(Path::new("Prayfile"));
270        assert_eq!(root, Path::new("."));
271    }
272
273    #[test]
274    fn project_root_from_manifest_uses_parent_directory() {
275        let root = project_root_from_manifest(Path::new("examples/simple-project/Prayfile"));
276        assert_eq!(root, Path::new("examples/simple-project"));
277    }
278
279    #[test]
280    fn discover_distribution_root_finds_root_and_prayers_subdirectory() {
281        let workspace =
282            std::env::temp_dir().join(format!("pray-discover-distribution-{}", std::process::id()));
283        let _ = fs::remove_dir_all(&workspace);
284        let repo_root = workspace.join("repo");
285        let prayers_root = repo_root.join("prayers");
286        fs::create_dir_all(prayers_root.join("v1/packages")).expect("prayers distribution");
287        fs::create_dir_all(repo_root.join("v1/packages")).expect("root distribution");
288
289        assert_eq!(
290            discover_distribution_root(&repo_root),
291            Some(repo_root.clone())
292        );
293
294        fs::remove_dir_all(repo_root.join("v1")).expect("remove root distribution");
295        assert_eq!(discover_distribution_root(&repo_root), Some(prayers_root));
296        let _ = fs::remove_dir_all(&workspace);
297    }
298
299    #[test]
300    fn discover_distribution_root_returns_none_without_registry_layout() {
301        let workspace =
302            std::env::temp_dir().join(format!("pray-discover-missing-{}", std::process::id()));
303        let _ = fs::remove_dir_all(&workspace);
304        let repo_root = workspace.join("repo");
305        fs::create_dir_all(&repo_root).expect("repo root");
306        assert_eq!(discover_distribution_root(&repo_root), None);
307        let _ = fs::remove_dir_all(&workspace);
308    }
309}