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::{
9 prepare_git_sources, prepare_pray_ssh_host_keys, GitSourceCheckout,
10};
11
12use crate::paths::find_prayspec_file;
13pub use crate::resolve_git::{discover_distribution_root, git_source_cache_directory};
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 pub registry_latest_version: Option<String>,
52 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: &BTreeMap<String, GitSourceCheckout>,
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}