1use anyhow::{Context, Result};
2use morph_config::{MorphLock, VersionFile};
3use sha2::{Digest, Sha256};
4use std::path::{Path, PathBuf};
5use walkdir::WalkDir;
6
7pub fn global_cache_root() -> Result<PathBuf> {
9 let home = dirs::home_dir().context("could not find home directory")?;
10 Ok(home.join(".morph"))
11}
12
13pub fn global_runtimes_dir() -> Result<PathBuf> {
14 Ok(global_cache_root()?.join("cache").join("runtimes"))
15}
16
17pub fn global_runtime_version_dir(runtime_type: &str, version: &str) -> Result<PathBuf> {
18 Ok(global_runtimes_dir()?.join(runtime_type).join(format!("v{}", version)))
19}
20
21pub fn is_runtime_cached(runtime_type: &str, version: &str) -> bool {
23 if let Ok(dir) = global_runtime_version_dir(runtime_type, version) {
24 dir.join("manifest.json").exists() || dir.join("include").exists()
25 } else {
26 false
27 }
28}
29
30#[derive(Debug, serde::Serialize, serde::Deserialize)]
32pub struct RuntimeManifest {
33 pub version: String,
34 pub runtime_type: String,
35 pub sha256: String,
36 pub size: u64,
37 pub cached_at: String,
38}
39
40pub fn sha256_file(path: &Path) -> Result<String> {
42 let bytes = std::fs::read(path)?;
43 let mut h = Sha256::new();
44 h.update(&bytes);
45 Ok(hex::encode(h.finalize()))
46}
47
48pub fn sha256_bytes(bytes: &[u8]) -> String {
50 let mut h = Sha256::new();
51 h.update(bytes);
52 hex::encode(h.finalize())
53}
54
55pub fn download_runtime(runtime_type: &str, version: &str) -> Result<PathBuf> {
57 let cached_dir = global_runtime_version_dir(runtime_type, version)?;
58
59 if is_runtime_cached(runtime_type, version) {
60 println!(" ✓ Found in cache: {}", cached_dir.display());
61 return Ok(cached_dir);
62 }
63
64 let local_runtime = find_local_runtime(runtime_type);
66 if let Some(local) = local_runtime {
67 println!(" ℹ Using local runtime from {}", local.display());
68 cache_local_runtime(&local, &cached_dir, runtime_type, version)?;
69 return Ok(cached_dir);
70 }
71
72 let url = format!(
74 "https://github.com/Levizr/morph/releases/download/runtime-{}-v{}/morph-runtime-{}-v{}.tar.gz",
75 runtime_type, version, runtime_type, version
76 );
77
78 println!(" Downloading {} v{} from GitHub...", runtime_type, version);
79 println!(" URL: {}", url);
80
81 let bytes = download_bytes(&url).with_context(|| format!("failed to download runtime {} v{} — check that release exists or use a local runtime/", runtime_type, version))?;
82
83 if bytes.starts_with(b"<!DOCTYPE") || bytes.starts_with(b"<html") {
85 anyhow::bail!("download returned HTML (release not found): {}", url);
86 }
87
88 std::fs::create_dir_all(&cached_dir)?;
89 extract_tar_gz(&bytes, &cached_dir)?;
90
91 let manifest = RuntimeManifest {
93 version: version.to_string(),
94 runtime_type: runtime_type.to_string(),
95 sha256: sha256_bytes(&bytes),
96 size: bytes.len() as u64,
97 cached_at: chrono_string(),
98 };
99 std::fs::write(
100 cached_dir.join("manifest.json"),
101 serde_json::to_string_pretty(&manifest)?,
102 )?;
103
104 println!(" ✓ Cached to {}", cached_dir.display());
105 Ok(cached_dir)
106}
107
108fn chrono_string() -> String {
109 std::time::SystemTime::now()
111 .duration_since(std::time::UNIX_EPOCH)
112 .map(|d| format!("{}", d.as_secs()))
113 .unwrap_or_default()
114}
115
116fn find_local_runtime(runtime_type: &str) -> Option<PathBuf> {
117 let mut candidates: Vec<PathBuf> = vec![
118 PathBuf::from(format!("runtime/{}", runtime_type)),
119 PathBuf::from(format!("../runtime/{}", runtime_type)),
120 PathBuf::from(format!("../../runtime/{}", runtime_type)),
121 PathBuf::from(format!("morph/runtime/{}", runtime_type)),
122 ];
123
124 if let Ok(exe) = std::env::current_exe() {
126 if let Some(exe_dir) = exe.parent() {
127 candidates.push(exe_dir.join(format!("../runtime/{}", runtime_type)));
128 candidates.push(exe_dir.join(format!("../../runtime/{}", runtime_type)));
129 candidates.push(exe_dir.join(format!("../../../runtime/{}", runtime_type)));
130 }
131 }
132
133 candidates.push(PathBuf::from(format!("/home/piyush/My_Projects/playground/morph/runtime/{}", runtime_type)));
135 candidates.push(PathBuf::from(format!("/home/piyush/My_Projects/morph/runtime/{}", runtime_type)));
136
137 if let Ok(cwd) = std::env::current_dir() {
139 let mut cur = cwd.clone();
140 for _ in 0..4 {
141 candidates.push(cur.join(format!("runtime/{}", runtime_type)));
142 if let Some(parent) = cur.parent() {
143 cur = parent.to_path_buf();
144 } else {
145 break;
146 }
147 }
148 }
149
150 for p in candidates {
151 if p.join("morph_api.h").exists() || p.join("include").exists() || p.exists() && p.is_dir() && std::fs::read_dir(&p).map(|mut d| d.next().is_some()).unwrap_or(false) {
152 return Some(p);
153 }
154 }
155 None
156}
157
158fn cache_local_runtime(src: &Path, dest: &Path, runtime_type: &str, version: &str) -> Result<()> {
159 std::fs::create_dir_all(dest)?;
160
161 if src.is_dir() {
163 copy_dir_recursive(src, dest)?;
164 }
165
166 if !dest.exists() || std::fs::read_dir(dest)?.next().is_none() {
168 std::fs::write(dest.join("README.md"), format!("# Morph Runtime {} v{} (stub)\n\nLocal runtime not yet populated. This is a placeholder.\n", runtime_type, version))?;
170 }
171
172 let manifest = RuntimeManifest {
173 version: version.to_string(),
174 runtime_type: runtime_type.to_string(),
175 sha256: "local".to_string(),
176 size: 0,
177 cached_at: chrono_string(),
178 };
179 std::fs::write(dest.join("manifest.json"), serde_json::to_string_pretty(&manifest)?)?;
180 Ok(())
181}
182
183fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<()> {
184 for entry in WalkDir::new(src).min_depth(1) {
185 let entry = entry?;
186 let rel = entry.path().strip_prefix(src)?;
187 let dest_path = dest.join(rel);
188 if entry.file_type().is_dir() {
189 std::fs::create_dir_all(&dest_path)?;
190 } else {
191 if let Some(parent) = dest_path.parent() {
192 std::fs::create_dir_all(parent)?;
193 }
194 std::fs::copy(entry.path(), &dest_path)?;
195 }
196 }
197 Ok(())
198}
199
200fn download_bytes(url: &str) -> Result<Vec<u8>> {
201 let client = reqwest::blocking::Client::builder()
202 .timeout(std::time::Duration::from_secs(60))
203 .user_agent("morphc/0.1.0")
204 .build()?;
205
206 let resp = client.get(url).send()?;
207
208 if !resp.status().is_success() {
209 anyhow::bail!("HTTP {} for {}", resp.status(), url);
210 }
211
212 Ok(resp.bytes()?.to_vec())
213}
214
215fn extract_tar_gz(bytes: &[u8], dest: &Path) -> Result<()> {
216 let gz = flate2::read::GzDecoder::new(bytes);
217 let mut archive = tar::Archive::new(gz);
218 archive.unpack(dest)?;
219 Ok(())
220}
221
222pub fn link_runtime_to_project(
224 runtime_type: &str,
225 version: &str,
226 project_morph_dir: &Path,
227) -> Result<PathBuf> {
228 let cached_dir = global_runtime_version_dir(runtime_type, version)?;
229 let project_runtime = project_morph_dir.join("runtime");
230
231 if project_runtime.exists() {
233 if project_runtime.is_symlink() {
234 std::fs::remove_file(&project_runtime)?;
235 } else {
236 std::fs::remove_dir_all(&project_runtime)?;
237 }
238 }
239
240 #[cfg(unix)]
242 {
243 if std::os::unix::fs::symlink(&cached_dir, &project_runtime).is_ok() {
244 return Ok(project_runtime);
245 }
246 }
247
248 copy_dir_recursive(&cached_dir, &project_runtime)?;
250 Ok(project_runtime)
251}
252
253pub fn fetch_latest_runtime_version(runtime_type: &str) -> Result<String> {
255 let local_version_file = PathBuf::from(format!("versions/runtime/{}.json", runtime_type));
257 if local_version_file.exists() {
258 let vf = VersionFile::from_file(&local_version_file)?;
259 return Ok(vf.version);
260 }
261
262 let _url = format!(
264 "https://api.github.com/repos/Levizr/morph/releases/tags/runtime-{}-v{}",
265 runtime_type, "latest"
266 );
267 anyhow::bail!("could not determine latest version for {}", runtime_type)
269}
270
271pub fn is_project_runtime_installed(project_morph_dir: &Path) -> bool {
273 let runtime = project_morph_dir.join("runtime");
274 runtime.exists() && (runtime.join("manifest.json").exists() || runtime.join("include").exists() || runtime.join("morph_api.h").exists() || std::fs::read_dir(&runtime).map(|mut d| d.next().is_some()).unwrap_or(false))
275}
276
277pub fn hash_tree(dir: &Path) -> String {
281 if !dir.is_dir() {
282 return String::new();
283 }
284 let mut entries: Vec<String> = Vec::new();
285 for entry in WalkDir::new(dir).follow_links(false).min_depth(1) {
286 let entry = match entry {
287 Ok(e) => e,
288 Err(_) => continue,
289 };
290 if entry.file_type().is_dir() {
291 continue;
292 }
293 let rel = match entry.path().strip_prefix(dir) {
294 Ok(r) => r.to_string_lossy().replace('\\', "/"),
295 Err(_) => continue,
296 };
297 if rel.split('/').any(|c| c == ".git" || c.starts_with('.')) {
298 continue;
299 }
300 let content = std::fs::read(entry.path()).unwrap_or_default();
301 let mut h = Sha256::new();
302 h.update(&content);
303 entries.push(format!("{}:{}", rel, hex::encode(h.finalize())));
304 }
305 entries.sort();
306 sha256_string(&entries.join("\n"))
307}
308
309pub fn project_hash_dir(cwd: &Path) -> PathBuf {
311 cwd.join(".morph").join("hash")
312}
313
314pub fn sha256_string(s: &str) -> String {
316 let mut h = Sha256::new();
317 h.update(s.as_bytes());
318 hex::encode(h.finalize())
319}
320
321pub fn fingerprint_inputs(inputs: &[(&str, &str)]) -> String {
326 let mut entries: Vec<String> = inputs
327 .iter()
328 .map(|(p, c)| format!("{}:{}\n", p, sha256_string(c)))
329 .collect();
330 entries.sort();
331 sha256_string(&entries.join(""))
332}
333
334pub fn read_stored_fingerprint(cwd: &Path, binary_name: &str) -> Option<String> {
336 let dir = project_hash_dir(cwd);
337 let file = dir.join(format!("{}.fingerprint", binary_name));
338 std::fs::read_to_string(&file).ok().map(|s| s.trim().to_string())
339}
340
341pub fn write_stored_fingerprint(cwd: &Path, binary_name: &str, fingerprint: &str) -> Result<()> {
343 let dir = project_hash_dir(cwd);
344 std::fs::create_dir_all(&dir)?;
345 std::fs::write(dir.join(format!("{}.fingerprint", binary_name)), fingerprint)?;
346 Ok(())
347}
348
349pub fn write_lock_file(
351 project_dir: &Path,
352 runtime_type: &str,
353 version: &str,
354 sha256: &str,
355) -> Result<()> {
356 let lock = MorphLock {
357 runtime: morph_config::LockRuntime {
358 runtime_type: runtime_type.to_string(),
359 version: version.to_string(),
360 sha256: sha256.to_string(),
361 downloaded_at: chrono_string(),
362 },
363 generated_by: format!("morphc {}", env!("CARGO_PKG_VERSION")),
364 generated_at: chrono_string(),
365 };
366 let path = project_dir.join("morph.lock");
367 std::fs::write(&path, serde_json::to_string_pretty(&lock)?)?;
368 Ok(())
369}
370
371