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