1use anyhow::{anyhow, bail, Context, Result};
2use arcstr::ArcStr;
3use async_trait::async_trait;
4use chrono::Local;
5use compact_str::{format_compact, CompactString};
6use crates_io_api::AsyncClient;
7use flate2::bufread::MultiGzDecoder;
8use fxhash::FxHashMap;
9use graphix_compiler::{env::Env, expr::ExprId, ExecCtx};
10use graphix_rt::{CompExp, GXExt, GXHandle, GXRt};
11use handlebars::Handlebars;
12pub use indexmap::IndexSet;
13use netidx_value::Value;
14use serde_json::json;
15use std::{
16 any::Any,
17 collections::BTreeMap,
18 path::{Path, PathBuf},
19 process::Stdio,
20 time::Duration,
21};
22use tokio::{
23 fs,
24 io::{AsyncBufReadExt, BufReader},
25 process::Command,
26 sync::oneshot,
27 task,
28};
29use walkdir::WalkDir;
30
31#[cfg(test)]
32mod test;
33
34#[async_trait]
36pub trait CustomDisplay<X: GXExt>: Any {
37 async fn clear(&mut self);
43
44 async fn process_update(&mut self, env: &Env, id: ExprId, v: Value);
51}
52
53pub trait Package<X: GXExt> {
55 fn register(
62 ctx: &mut ExecCtx<GXRt<X>, X::UserEvent>,
63 modules: &mut FxHashMap<netidx_core::path::Path, ArcStr>,
64 root_mods: &mut IndexSet<ArcStr>,
65 ) -> Result<()>;
66
67 fn is_custom(gx: &GXHandle<X>, env: &Env, e: &CompExp<X>) -> bool;
70
71 fn init_custom(
79 gx: &GXHandle<X>,
80 env: &Env,
81 stop: oneshot::Sender<()>,
82 e: CompExp<X>,
83 ) -> Result<Box<dyn CustomDisplay<X>>>;
84
85 fn main_program() -> Option<&'static str>;
88}
89
90struct Skel {
92 version: &'static str,
93 cargo_toml: &'static str,
94 deps_rs: &'static str,
95 lib_rs: &'static str,
96 mod_gx: &'static str,
97 mod_gxi: &'static str,
98 readme_md: &'static str,
99}
100
101static SKEL: Skel = Skel {
102 version: env!("CARGO_PKG_VERSION"),
103 cargo_toml: include_str!("skel/Cargo.toml.hbs"),
104 deps_rs: include_str!("skel/deps.rs"),
105 lib_rs: include_str!("skel/lib.rs"),
106 mod_gx: include_str!("skel/mod.gx"),
107 mod_gxi: include_str!("skel/mod.gxi"),
108 readme_md: include_str!("skel/README.md"),
109};
110
111pub async fn create_package(base: &Path, name: &str) -> Result<()> {
117 if !fs::metadata(base).await?.is_dir() {
118 bail!("base path {base:?} does not exist, or is not a directory")
119 }
120 if name.contains(|c: char| c != '-' && !c.is_ascii_alphanumeric())
121 || !name.starts_with("graphix-package-")
122 {
123 bail!("invalid package name, name must match graphix-package-[-a-z]+")
124 }
125 let full_path = base.join(name);
126 if fs::metadata(&full_path).await.is_ok() {
127 bail!("package {name} already exists")
128 }
129 fs::create_dir_all(&full_path.join("src").join("graphix")).await?;
130 let mut hb = Handlebars::new();
131 hb.register_template_string("Cargo.toml", SKEL.cargo_toml)?;
132 hb.register_template_string("lib.rs", SKEL.lib_rs)?;
133 hb.register_template_string("mod.gx", SKEL.mod_gx)?;
134 hb.register_template_string("mod.gxi", SKEL.mod_gxi)?;
135 hb.register_template_string("README.md", SKEL.readme_md)?;
136 let name = name.strip_prefix("graphix-package-").unwrap();
137 let params = json!({"name": name, "deps": []});
138 fs::write(full_path.join("Cargo.toml"), hb.render("Cargo.toml", ¶ms)?).await?;
139 fs::write(full_path.join("README.md"), hb.render("README.md", ¶ms)?).await?;
140 let src = full_path.join("src");
141 fs::write(src.join("lib.rs"), hb.render("lib.rs", ¶ms)?).await?;
142 let graphix_src = src.join("graphix");
143 fs::write(&graphix_src.join("mod.gx"), hb.render("mod.gx", ¶ms)?).await?;
144 fs::write(&graphix_src.join("mod.gxi"), hb.render("mod.gxi", ¶ms)?).await?;
145 Ok(())
146}
147
148fn graphix_data_dir() -> Result<PathBuf> {
149 Ok(dirs::data_local_dir()
150 .ok_or_else(|| anyhow!("can't find your data dir"))?
151 .join("graphix"))
152}
153
154fn packages_toml_path() -> Result<PathBuf> {
155 Ok(graphix_data_dir()?.join("packages.toml"))
156}
157
158const DEFAULT_PACKAGES: &[(&str, &str)] = &[
160 ("core", SKEL.version),
161 ("array", SKEL.version),
162 ("str", SKEL.version),
163 ("map", SKEL.version),
164 ("fs", SKEL.version),
165 ("time", SKEL.version),
166 ("net", SKEL.version),
167 ("re", SKEL.version),
168 ("rand", SKEL.version),
169 ("tui", SKEL.version),
170];
171
172fn is_stdlib_package(name: &str) -> bool {
173 DEFAULT_PACKAGES.iter().any(|(n, _)| *n == name)
174}
175
176#[derive(Debug, Clone)]
178pub enum PackageEntry {
179 Version(String),
180 Path(PathBuf),
181}
182
183impl std::fmt::Display for PackageEntry {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 match self {
186 Self::Version(v) => write!(f, "{v}"),
187 Self::Path(p) => write!(f, "path:{}", p.display()),
188 }
189 }
190}
191
192async fn read_packages() -> Result<BTreeMap<String, PackageEntry>> {
194 let path = packages_toml_path()?;
195 match fs::read_to_string(&path).await {
196 Ok(contents) => {
197 let doc: toml::Value =
198 toml::from_str(&contents).context("parsing packages.toml")?;
199 let tbl = doc
200 .get("packages")
201 .and_then(|v| v.as_table())
202 .ok_or_else(|| anyhow!("packages.toml missing [packages] table"))?;
203 let mut packages = BTreeMap::new();
204 for (k, v) in tbl {
205 let entry = match v {
206 toml::Value::String(s) => PackageEntry::Version(s.clone()),
207 toml::Value::Table(t) => {
208 if let Some(p) = t.get("path").and_then(|v| v.as_str()) {
209 PackageEntry::Path(PathBuf::from(p))
210 } else {
211 bail!("package {k}: table entry must have a 'path' key")
212 }
213 }
214 _ => bail!("package {k}: expected a version string or table"),
215 };
216 packages.insert(k.clone(), entry);
217 }
218 Ok(packages)
219 }
220 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
221 let packages: BTreeMap<String, PackageEntry> = DEFAULT_PACKAGES
222 .iter()
223 .map(|(k, v)| (k.to_string(), PackageEntry::Version(v.to_string())))
224 .collect();
225 write_packages(&packages).await?;
226 Ok(packages)
227 }
228 Err(e) => Err(e.into()),
229 }
230}
231
232async fn write_packages(packages: &BTreeMap<String, PackageEntry>) -> Result<()> {
234 let path = packages_toml_path()?;
235 if let Some(parent) = path.parent() {
236 fs::create_dir_all(parent).await?;
237 }
238 let mut doc = toml::value::Table::new();
239 let mut tbl = toml::value::Table::new();
240 for (k, entry) in packages {
241 match entry {
242 PackageEntry::Version(v) => {
243 tbl.insert(k.clone(), toml::Value::String(v.clone()));
244 }
245 PackageEntry::Path(p) => {
246 let mut t = toml::value::Table::new();
247 t.insert(
248 "path".to_string(),
249 toml::Value::String(p.to_string_lossy().into_owned()),
250 );
251 tbl.insert(k.clone(), toml::Value::Table(t));
252 }
253 }
254 }
255 doc.insert("packages".to_string(), toml::Value::Table(tbl));
256 fs::write(&path, toml::to_string_pretty(&doc)?).await?;
257 Ok(())
258}
259
260async fn graphix_version() -> Result<String> {
262 let graphix = which::which("graphix").context("can't find the graphix command")?;
263 let c = Command::new(&graphix).arg("--version").stdout(Stdio::piped()).spawn()?;
264 let line = BufReader::new(c.stdout.unwrap())
265 .lines()
266 .next_line()
267 .await?
268 .ok_or_else(|| anyhow!("graphix did not return a version"))?;
269 Ok(line.split_whitespace().last().unwrap_or(&line).to_string())
271}
272
273async fn extract_local_source(cargo: &Path, version: &str) -> Result<PathBuf> {
275 let graphix_build_dir = graphix_data_dir()?.join("build");
276 let graphix_dir = graphix_build_dir.join(format!("graphix-shell-{version}"));
277 match fs::metadata(&graphix_build_dir).await {
278 Err(_) => fs::create_dir_all(&graphix_build_dir).await?,
279 Ok(md) if !md.is_dir() => bail!("{graphix_build_dir:?} isn't a directory"),
280 Ok(_) => (),
281 }
282 match fs::metadata(&graphix_dir).await {
283 Ok(md) if !md.is_dir() => bail!("{graphix_dir:?} isn't a directory"),
284 Ok(_) => return Ok(graphix_dir),
285 Err(_) => (),
286 }
287 let package = format!("graphix-shell-{version}");
288 let cargo_root = cargo
289 .parent()
290 .ok_or_else(|| anyhow!("can't find cargo root"))?
291 .parent()
292 .ok_or_else(|| anyhow!("can't find cargo root"))?;
293 let cargo_src = cargo_root.join("registry").join("src");
294 match fs::metadata(&cargo_src).await {
295 Ok(md) if md.is_dir() => (),
296 Err(_) | Ok(_) => bail!("can't find cargo cache {cargo_src:?}"),
297 };
298 let r = task::spawn_blocking({
299 let graphix_dir = graphix_dir.clone();
300 move || -> Result<()> {
301 let src_path = WalkDir::new(&cargo_src)
302 .max_depth(2)
303 .into_iter()
304 .find_map(|e| {
305 let e = e.ok()?;
306 if e.file_type().is_dir() && e.path().ends_with(&package) {
307 return Some(e.into_path());
308 }
309 None
310 })
311 .ok_or_else(|| anyhow!("can't find {package} in {cargo_src:?}"))?;
312 cp_r::CopyOptions::new().copy_tree(&src_path, graphix_dir)?;
313 Ok(())
314 }
315 })
316 .await?;
317 match r {
318 Ok(()) => Ok(graphix_dir),
319 Err(e) => {
320 let _ = fs::remove_dir_all(&graphix_dir).await;
321 Err(e)
322 }
323 }
324}
325
326async fn download_source(crates_io: &AsyncClient, version: &str) -> Result<PathBuf> {
328 let package = format!("graphix-shell-{version}");
329 let graphix_build_dir = graphix_data_dir()?.join("build");
330 let graphix_dir = graphix_build_dir.join(&package);
331 match fs::metadata(&graphix_build_dir).await {
332 Err(_) => fs::create_dir_all(&graphix_build_dir).await?,
333 Ok(md) if !md.is_dir() => bail!("{graphix_build_dir:?} isn't a directory"),
334 Ok(_) => (),
335 }
336 match fs::metadata(&graphix_dir).await {
337 Ok(md) if !md.is_dir() => bail!("{graphix_dir:?} isn't a directory"),
338 Ok(_) => return Ok(graphix_dir),
339 Err(_) => (),
340 }
341 let cr = crates_io.get_crate("graphix-shell").await?;
342 let cr_version = cr
343 .versions
344 .into_iter()
345 .find(|v| v.num == version)
346 .ok_or_else(|| anyhow!("can't find version {version} on crates.io"))?;
347 let crate_data_tar_gz = reqwest::get(&cr_version.dl_path).await?.bytes().await?;
348 let r = task::spawn_blocking({
349 let graphix_dir = graphix_dir.clone();
350 move || -> Result<()> {
351 use std::io::Read;
352 let mut crate_data_tar = vec![];
353 MultiGzDecoder::new(&crate_data_tar_gz[..])
354 .read_to_end(&mut crate_data_tar)?;
355 std::fs::create_dir_all(&graphix_dir)?;
356 tar::Archive::new(&mut &crate_data_tar[..]).unpack(&graphix_dir)?;
357 Ok(())
358 }
359 })
360 .await?;
361 match r {
362 Ok(()) => Ok(graphix_dir),
363 Err(e) => {
364 let _ = fs::remove_dir_all(&graphix_dir).await;
365 Err(e)
366 }
367 }
368}
369
370#[derive(Debug, Clone)]
371pub struct PackageId {
372 name: CompactString,
373 version: Option<CompactString>,
374 path: Option<PathBuf>,
375}
376
377impl PackageId {
378 pub fn new(name: &str, version: Option<&str>) -> Self {
379 let name = if name.starts_with("graphix-package-") {
380 CompactString::from(name.strip_prefix("graphix-package-").unwrap())
381 } else {
382 CompactString::from(name)
383 };
384 let version = version.map(CompactString::from);
385 Self { name, version, path: None }
386 }
387
388 pub fn with_path(name: &str, path: PathBuf) -> Self {
389 let name = if name.starts_with("graphix-package-") {
390 CompactString::from(name.strip_prefix("graphix-package-").unwrap())
391 } else {
392 CompactString::from(name)
393 };
394 Self { name, version: None, path: Some(path) }
395 }
396
397 pub fn name(&self) -> &str {
399 &self.name
400 }
401
402 pub fn crate_name(&self) -> CompactString {
404 format_compact!("graphix-package-{}", self.name)
405 }
406
407 pub fn version(&self) -> Option<&str> {
408 self.version.as_ref().map(|s| s.as_str())
409 }
410
411 pub fn path(&self) -> Option<&Path> {
412 self.path.as_deref()
413 }
414}
415
416pub struct GraphixPM {
418 cratesio: AsyncClient,
419 cargo: PathBuf,
420}
421
422impl GraphixPM {
423 pub async fn new() -> Result<Self> {
425 let cargo = which::which("cargo").context("can't find the cargo command")?;
426 let cratesio = AsyncClient::new(
427 "Graphix Package Manager <eestokes@pm.me>",
428 Duration::from_secs(1),
429 )?;
430 Ok(Self { cratesio, cargo })
431 }
432
433 fn lock_file() -> Result<fd_lock::RwLock<std::fs::File>> {
436 let lock_path = graphix_data_dir()?.join("graphix.lock");
437 if let Some(parent) = lock_path.parent() {
438 std::fs::create_dir_all(parent)?;
439 }
440 let file = std::fs::OpenOptions::new()
441 .create(true)
442 .truncate(false)
443 .read(true)
444 .write(true)
445 .open(&lock_path)
446 .context("opening lock file")?;
447 Ok(fd_lock::RwLock::new(file))
448 }
449
450 async fn unpack_source(&self, version: &str) -> Result<PathBuf> {
454 match extract_local_source(&self.cargo, version).await {
455 Ok(p) => Ok(p),
456 Err(local) => match download_source(&self.cratesio, version).await {
457 Ok(p) => Ok(p),
458 Err(dl) => bail!("could not find our source local: {local}, dl: {dl}"),
459 },
460 }
461 }
462
463 fn generate_deps_rs(
465 &self,
466 packages: &BTreeMap<String, PackageEntry>,
467 ) -> Result<String> {
468 let mut hb = Handlebars::new();
469 hb.register_template_string("deps.rs", SKEL.deps_rs)?;
470 let deps: Vec<serde_json::Value> = packages
471 .keys()
472 .map(|name| {
473 json!({
474 "crate_name": format!("graphix_package_{}", name.replace('-', "_")),
475 })
476 })
477 .collect();
478 let params = json!({ "deps": deps });
479 Ok(hb.render("deps.rs", ¶ms)?)
480 }
481
482 fn update_cargo_toml(
484 &self,
485 cargo_toml_content: &str,
486 packages: &BTreeMap<String, PackageEntry>,
487 ) -> Result<String> {
488 use toml_edit::DocumentMut;
489 let mut doc: DocumentMut =
490 cargo_toml_content.parse().context("parsing Cargo.toml")?;
491 let deps = doc["dependencies"]
492 .as_table_mut()
493 .ok_or_else(|| anyhow!("Cargo.toml missing [dependencies]"))?;
494 let to_remove: Vec<String> = deps
495 .iter()
496 .filter_map(|(k, _)| {
497 if k.starts_with("graphix-package-") {
498 Some(k.to_string())
499 } else {
500 None
501 }
502 })
503 .collect();
504 for k in to_remove {
505 deps.remove(&k);
506 }
507 for (name, entry) in packages {
508 let crate_name = format!("graphix-package-{name}");
509 match entry {
510 PackageEntry::Version(version) => {
511 deps[&crate_name] = toml_edit::value(version);
512 }
513 PackageEntry::Path(path) => {
514 let mut tbl = toml_edit::InlineTable::new();
515 tbl.insert(
516 "path",
517 toml_edit::Value::from(path.to_string_lossy().as_ref()),
518 );
519 deps[&crate_name] = toml_edit::Item::Value(tbl.into());
520 }
521 }
522 }
523 Ok(doc.to_string())
524 }
525
526 async fn rebuild(
528 &self,
529 packages: &BTreeMap<String, PackageEntry>,
530 version: &str,
531 ) -> Result<()> {
532 println!("Unpacking graphix-shell source...");
533 let build_dir = graphix_data_dir()?.join("build");
535 if fs::metadata(&build_dir).await.is_ok() {
536 fs::remove_dir_all(&build_dir).await?;
537 }
538 let source_dir = self.unpack_source(version).await?;
539 println!("Generating deps.rs...");
541 let deps_rs = self.generate_deps_rs(&packages)?;
542 fs::write(source_dir.join("src").join("deps.rs"), &deps_rs).await?;
543 println!("Updating Cargo.toml...");
545 let cargo_toml_path = source_dir.join("Cargo.toml");
546 let cargo_toml_content = fs::read_to_string(&cargo_toml_path).await?;
547 let updated_cargo_toml =
548 self.update_cargo_toml(&cargo_toml_content, &packages)?;
549 fs::write(&cargo_toml_path, &updated_cargo_toml).await?;
550 if let Ok(graphix_path) = which::which("graphix") {
552 let date = Local::now().format("%Y%m%d-%H%M%S");
553 let backup_name = format!(
554 "graphix-previous-{date}{}",
555 graphix_path
556 .extension()
557 .map(|e| format!(".{}", e.to_string_lossy()))
558 .unwrap_or_default()
559 );
560 let backup_path = graphix_path.with_file_name(&backup_name);
561 let _ = fs::copy(&graphix_path, &backup_path).await;
562 }
563 println!("Building graphix with updated packages (this may take a while)...");
565 let status = Command::new(&self.cargo)
566 .arg("install")
567 .arg("--path")
568 .arg(&source_dir)
569 .arg("--force")
570 .status()
571 .await
572 .context("running cargo install")?;
573 if !status.success() {
574 bail!("cargo install failed with status {status}")
575 }
576 self.cleanup_old_binaries().await;
578 println!("Done! Restart graphix to use the updated packages.");
579 Ok(())
580 }
581
582 async fn cleanup_old_binaries(&self) {
584 let Ok(graphix_path) = which::which("graphix") else { return };
585 let Some(bin_dir) = graphix_path.parent() else { return };
586 let Ok(mut entries) = fs::read_dir(bin_dir).await else { return };
587 let week_ago =
588 std::time::SystemTime::now() - std::time::Duration::from_secs(7 * 24 * 3600);
589 while let Ok(Some(entry)) = entries.next_entry().await {
590 let name = entry.file_name();
591 let Some(name) = name.to_str() else { continue };
592 if !name.starts_with("graphix-previous-") {
593 continue;
594 }
595 if let Ok(md) = entry.metadata().await {
596 if let Ok(modified) = md.modified() {
597 if modified < week_ago {
598 let _ = fs::remove_file(entry.path()).await;
599 }
600 }
601 }
602 }
603 }
604
605 async fn read_package_version(path: &Path) -> Result<String> {
607 let cargo_toml_path = path.join("Cargo.toml");
608 let contents = fs::read_to_string(&cargo_toml_path)
609 .await
610 .with_context(|| format!("reading {}", cargo_toml_path.display()))?;
611 let doc: toml::Value =
612 toml::from_str(&contents).context("parsing package Cargo.toml")?;
613 doc.get("package")
614 .and_then(|p| p.get("version"))
615 .and_then(|v| v.as_str())
616 .map(|s| s.to_string())
617 .ok_or_else(|| anyhow!("no version found in {}", cargo_toml_path.display()))
618 }
619
620 pub async fn add_packages(
622 &self,
623 packages: &[PackageId],
624 skip_crates_io_check: bool,
625 ) -> Result<()> {
626 let mut lock = Self::lock_file()?;
627 let _guard = lock.write().context("waiting for package lock")?;
628 let mut installed = read_packages().await?;
629 let mut changed = false;
630 for pkg in packages {
631 let entry = if let Some(path) = pkg.path() {
632 let path = path
633 .canonicalize()
634 .with_context(|| format!("resolving path {}", path.display()))?;
635 let version = Self::read_package_version(&path).await?;
636 println!(
637 "Adding {} @ path {} (version {version})",
638 pkg.name(),
639 path.display()
640 );
641 PackageEntry::Path(path)
642 } else if skip_crates_io_check {
643 match pkg.version() {
644 Some(v) => {
645 println!("Adding {}@{v}", pkg.name());
646 PackageEntry::Version(v.to_string())
647 }
648 None => bail!(
649 "version is required for {} when using --skip-crates-io-check",
650 pkg.name()
651 ),
652 }
653 } else {
654 let crate_name = pkg.crate_name();
655 let cr =
656 self.cratesio.get_crate(&crate_name).await.with_context(|| {
657 format!("package {crate_name} not found on crates.io")
658 })?;
659 let version = match pkg.version() {
660 Some(v) => v.to_string(),
661 None => cr.crate_data.max_version.clone(),
662 };
663 println!("Adding {}@{version}", pkg.name());
664 PackageEntry::Version(version)
665 };
666 installed.insert(pkg.name().to_string(), entry);
667 changed = true;
668 }
669 if changed {
670 let version = graphix_version().await?;
671 self.rebuild(&installed, &version).await?;
672 write_packages(&installed).await?;
673 } else {
674 println!("No changes needed.");
675 }
676 Ok(())
677 }
678
679 pub async fn remove_packages(&self, packages: &[PackageId]) -> Result<()> {
681 let mut lock = Self::lock_file()?;
682 let _guard = lock.write().context("waiting for package lock")?;
683 let mut installed = read_packages().await?;
684 let mut changed = false;
685 for pkg in packages {
686 if pkg.name() == "core" {
687 eprintln!("Cannot remove the core package");
688 continue;
689 }
690 if installed.remove(pkg.name()).is_some() {
691 println!("Removing {}", pkg.name());
692 changed = true;
693 } else {
694 println!("{} is not installed", pkg.name());
695 }
696 }
697 if changed {
698 let version = graphix_version().await?;
699 self.rebuild(&installed, &version).await?;
700 write_packages(&installed).await?;
701 } else {
702 println!("No changes needed.");
703 }
704 Ok(())
705 }
706
707 pub async fn search(&self, query: &str) -> Result<()> {
709 let search_query = format!("graphix-package-{query}");
710 let results = self
711 .cratesio
712 .crates(crates_io_api::CratesQuery::builder().search(&search_query).build())
713 .await?;
714 if results.crates.is_empty() {
715 println!("No packages found matching '{query}'");
716 } else {
717 for cr in &results.crates {
718 let name = cr.name.strip_prefix("graphix-package-").unwrap_or(&cr.name);
719 let desc = cr.description.as_deref().unwrap_or("");
720 println!("{name} ({}) - {desc}", cr.max_version);
721 }
722 }
723 Ok(())
724 }
725
726 pub async fn do_rebuild(&self) -> Result<()> {
728 let mut lock = Self::lock_file()?;
729 let _guard = lock.write().context("waiting for package lock")?;
730 let packages = read_packages().await?;
731 let version = graphix_version().await?;
732 self.rebuild(&packages, &version).await
733 }
734
735 pub async fn list(&self) -> Result<()> {
737 let packages = read_packages().await?;
738 if packages.is_empty() {
739 println!("No packages installed");
740 } else {
741 for (name, version) in &packages {
742 println!("{name}: {version}");
743 }
744 }
745 Ok(())
746 }
747
748 pub async fn build_standalone(
754 &self,
755 package_dir: &Path,
756 source_override: Option<&Path>,
757 ) -> Result<()> {
758 let package_dir = package_dir
759 .canonicalize()
760 .with_context(|| format!("resolving {}", package_dir.display()))?;
761 let cargo_toml_path = package_dir.join("Cargo.toml");
763 let contents = fs::read_to_string(&cargo_toml_path)
764 .await
765 .with_context(|| format!("reading {}", cargo_toml_path.display()))?;
766 let doc: toml::Value =
767 toml::from_str(&contents).context("parsing package Cargo.toml")?;
768 let crate_name = doc
769 .get("package")
770 .and_then(|p| p.get("name"))
771 .and_then(|v| v.as_str())
772 .ok_or_else(|| anyhow!("no package name in {}", cargo_toml_path.display()))?;
773 let short_name =
774 crate_name.strip_prefix("graphix-package-").ok_or_else(|| {
775 anyhow!("package name must start with graphix-package-, got {crate_name}")
776 })?;
777 let mut packages = BTreeMap::new();
778 packages.insert(short_name.to_string(), PackageEntry::Path(package_dir.clone()));
779 let mut lock_storage =
780 if source_override.is_none() { Some(Self::lock_file()?) } else { None };
781 let _guard = lock_storage
782 .as_mut()
783 .map(|l| l.write().context("waiting for package lock"))
784 .transpose()?;
785 let source_dir = if let Some(dir) = source_override {
786 dir.to_path_buf()
787 } else {
788 println!("Unpacking graphix-shell source...");
789 let build_dir = graphix_data_dir()?.join("build");
790 if fs::metadata(&build_dir).await.is_ok() {
791 fs::remove_dir_all(&build_dir).await?;
792 }
793 self.unpack_source(&graphix_version().await?).await?
794 };
795 println!("Generating deps.rs...");
796 let deps_rs = self.generate_deps_rs(&packages)?;
797 fs::write(source_dir.join("src").join("deps.rs"), &deps_rs).await?;
798 println!("Updating Cargo.toml...");
799 let shell_cargo_toml_path = source_dir.join("Cargo.toml");
800 let shell_cargo_toml = fs::read_to_string(&shell_cargo_toml_path).await?;
801 let updated = self.update_cargo_toml(&shell_cargo_toml, &packages)?;
802 fs::write(&shell_cargo_toml_path, &updated).await?;
803 println!("Building standalone binary (this may take a while)...");
804 let status = Command::new(&self.cargo)
805 .arg("build")
806 .arg("--release")
807 .arg("--features")
808 .arg(format!("{crate_name}/standalone"))
809 .current_dir(&source_dir)
810 .status()
811 .await
812 .context("running cargo build")?;
813 if !status.success() {
814 bail!("cargo build --release failed with status {status}")
815 }
816 let bin_name = format!("{short_name}{}", std::env::consts::EXE_SUFFIX);
817 let built = source_dir
818 .join("target")
819 .join("release")
820 .join(format!("graphix{}", std::env::consts::EXE_SUFFIX));
821 let dest = package_dir.join(&bin_name);
822 fs::copy(&built, &dest).await.with_context(|| {
823 format!("copying {} to {}", built.display(), dest.display())
824 })?;
825 println!("Done! Binary written to {}", dest.display());
826 Ok(())
827 }
828
829 async fn latest_version(&self, crate_name: &str) -> Result<String> {
831 let cr = self
832 .cratesio
833 .get_crate(crate_name)
834 .await
835 .with_context(|| format!("querying crates.io for {crate_name}"))?;
836 Ok(cr.crate_data.max_version)
837 }
838
839 pub async fn update(&self) -> Result<()> {
841 let mut lock = Self::lock_file()?;
842 let _guard = lock.write().context("waiting for package lock")?;
843 let current = graphix_version().await?;
844 let latest_shell = self.latest_version("graphix-shell").await?;
845 if current == latest_shell {
846 println!("graphix is already up to date (version {current})");
847 return Ok(());
848 }
849 println!("Updating graphix from {current} to {latest_shell}...");
850 let mut packages = read_packages().await?;
851 for (name, entry) in packages.iter_mut() {
852 if is_stdlib_package(name) {
853 if let PackageEntry::Version(_) = entry {
854 let crate_name = format!("graphix-package-{name}");
855 let latest = self.latest_version(&crate_name).await?;
856 println!(" {name}: {entry} -> {latest}");
857 *entry = PackageEntry::Version(latest);
858 }
859 }
860 }
861 self.rebuild(&packages, &latest_shell).await?;
862 write_packages(&packages).await?;
863 Ok(())
864 }
865}