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 pub registry_latest_version: Option<String>,
53 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}