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