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::{discover_distribution_root, git_source_cache_directory};
12pub use crate::resolve_git_lookup::git_source_cached_repository;
13pub use crate::resolve_git_paths::git_source_cache_directory_with_subdir;
14pub use crate::resolve_git_refresh::{
15    annotate_failed_git_refresh, annotate_missing_git_catalog,
16    resolution_may_benefit_from_git_source_refresh,
17};
18pub use crate::resolve_git_sources::refresh_git_sources;
19use crate::{PrayError, PrayResult};
20use std::collections::BTreeMap;
21use std::fs;
22use std::path::{Path, PathBuf};
23
24#[derive(Debug, Clone)]
25pub struct ResolvedProject {
26    pub manifest_path: PathBuf,
27    pub project_root: PathBuf,
28    pub manifest: Manifest,
29    pub manifest_hash: String,
30    pub packages: Vec<ResolvedPackage>,
31    pub local_files: Vec<ResolvedLocalFile>,
32    pub source_revisions: BTreeMap<String, String>,
33    pub source_host_keys: BTreeMap<String, String>,
34    pub environment: Option<String>,
35}
36
37#[derive(Debug, Clone)]
38pub struct ResolvedPackage {
39    pub declaration: ManifestPackage,
40    pub root: PathBuf,
41    pub spec: PackageSpec,
42    pub tree_hash: String,
43    pub artifact_hash: String,
44    pub artifact: String,
45    pub selected_exports: Vec<String>,
46    pub source_checksum: String,
47    pub export_bodies: BTreeMap<String, String>,
48    pub skill_files: BTreeMap<String, Vec<String>>,
49    pub signer_fingerprint: Option<String>,
50    /// Highest non-yanked version in registry metadata when the package came from a registry source.
51    pub registry_latest_version: Option<String>,
52    /// True when the package was declared in Prayfile; false for transitive dependencies.
53    pub explicit: bool,
54    pub upstream: Option<crate::package_upstream::LockedUpstream>,
55}
56
57#[derive(Debug, Clone)]
58pub struct ResolvedLocalFile {
59    pub path: PathBuf,
60    pub manifest_path: String,
61    pub content: String,
62    pub source_checksum: 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_implied_source.rs"]
138mod implied_source;
139pub(crate) use implied_source::implied_source_name;
140
141#[path = "resolve_package_root.rs"]
142mod package_root;
143pub(crate) use package_root::{resolve_package_root, PackageRootResolution};
144
145#[path = "resolve_upstream.rs"]
146pub mod resolve_upstream;
147pub use resolve_upstream::{apply_path_upstream_refreshes, path_fork_drift_lines};
148
149fn resolve_package(
150    project_root: &Path,
151    sources: &BTreeMap<String, ManifestSource>,
152    git_sources: &GitSourceSet,
153    user_config: &crate::config::PrayConfig,
154    declaration: &ManifestPackage,
155    lockfile: Option<&Lockfile>,
156    options: &ResolveOptions,
157) -> PrayResult<ResolvedPackage> {
158    let PackageRootResolution {
159        root,
160        signer_fingerprint,
161        registry_latest_version,
162    } = resolve_package_root(
163        project_root,
164        sources,
165        git_sources,
166        user_config,
167        declaration,
168        lockfile,
169        options,
170    )?;
171    let spec_path = find_prayspec_file(&root)?;
172    let spec_text = fs::read_to_string(&spec_path)?;
173    let spec = parse_package_spec(&spec_text)?.canonicalized();
174    if spec.name != declaration.name {
175        return Err(PrayError::Resolution(format!(
176            "package path {:?} declares {:?}, expected {:?}",
177            root, spec.name, declaration.name
178        )));
179    }
180    spec.satisfy_constraint(&declaration.constraint)?;
181    let selected_exports = select_exports(declaration, &spec)?;
182    let file_bytes = load_package_file_bytes(&root, &spec)?;
183    let tree_hash = PackageSpec::tree_hash_from_file_bytes(&file_bytes)?;
184    let export_bodies = load_export_bodies(&file_bytes, &spec, &selected_exports)?;
185    let skill_files = build_skill_file_index(&spec);
186    let source_checksum = tree_hash.clone();
187    let upstream_context = resolve_upstream::UpstreamResolutionContext::new(
188        project_root,
189        sources,
190        git_sources,
191        user_config,
192        lockfile,
193        options,
194    );
195    let upstream = resolve_upstream::lock_path_upstream(&upstream_context, declaration, &spec)?;
196    Ok(ResolvedPackage {
197        declaration: declaration.clone(),
198        root,
199        spec: spec.clone(),
200        tree_hash: tree_hash.clone(),
201        artifact_hash: tree_hash.clone(),
202        artifact: format!(
203            "path:{}",
204            spec_path.parent().unwrap_or(&spec_path).to_string_lossy()
205        ),
206        selected_exports,
207        source_checksum,
208        export_bodies,
209        skill_files,
210        signer_fingerprint,
211        registry_latest_version,
212        explicit: false,
213        upstream,
214    })
215}
216
217#[path = "resolve_local.rs"]
218mod local;
219pub use local::missing_local_embed_guidance;
220pub(crate) use local::resolve_local_file;
221
222fn source_map(sources: &[ManifestSource]) -> BTreeMap<String, ManifestSource> {
223    sources
224        .iter()
225        .map(|source| (source.name.clone(), source.clone()))
226        .collect()
227}
228
229#[cfg(test)]
230mod tests {
231    use super::{discover_distribution_root, project_root_from_manifest};
232    use std::fs;
233    use std::path::Path;
234
235    #[test]
236    fn project_root_from_manifest_uses_cwd_for_bare_filename() {
237        let root = project_root_from_manifest(Path::new("Prayfile"));
238        assert_eq!(root, Path::new("."));
239    }
240
241    #[test]
242    fn project_root_from_manifest_uses_parent_directory() {
243        let root = project_root_from_manifest(Path::new("examples/simple-project/Prayfile"));
244        assert_eq!(root, Path::new("examples/simple-project"));
245    }
246
247    #[test]
248    fn discover_distribution_root_finds_root_and_prayers_subdirectory() {
249        let workspace =
250            std::env::temp_dir().join(format!("pray-discover-distribution-{}", std::process::id()));
251        let _ = fs::remove_dir_all(&workspace);
252        let repo_root = workspace.join("repo");
253        let prayers_root = repo_root.join("prayers");
254        fs::create_dir_all(prayers_root.join("v1/packages")).expect("prayers distribution");
255        fs::create_dir_all(repo_root.join("v1/packages")).expect("root distribution");
256
257        assert_eq!(
258            discover_distribution_root(&repo_root),
259            Some(repo_root.clone())
260        );
261
262        fs::remove_dir_all(repo_root.join("v1")).expect("remove root distribution");
263        assert_eq!(discover_distribution_root(&repo_root), Some(prayers_root));
264        let _ = fs::remove_dir_all(&workspace);
265    }
266
267    #[test]
268    fn discover_distribution_root_returns_none_without_registry_layout() {
269        let workspace =
270            std::env::temp_dir().join(format!("pray-discover-missing-{}", std::process::id()));
271        let _ = fs::remove_dir_all(&workspace);
272        let repo_root = workspace.join("repo");
273        fs::create_dir_all(&repo_root).expect("repo root");
274        assert_eq!(discover_distribution_root(&repo_root), None);
275        let _ = fs::remove_dir_all(&workspace);
276    }
277}