Skip to main content

pray_core/
resolve.rs

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