Skip to main content

interstice_cli/
module.rs

1use crate::node_client::handshake_with_node;
2use crate::node_registry::NodeRegistry;
3use interstice_core::{IntersticeError, ModuleEventInstance, NetworkPacket, packet::write_packet};
4use serde_json::Value;
5use std::path::{Path, PathBuf};
6
7pub async fn publish(node_ref: String, module_project_path: &Path) -> Result<(), IntersticeError> {
8    // This should take a path to a rust project that it will build and publish. The module name is from Cargo.toml. It should build the project using cargo, then read the generated wasm file and send it to the node using the network module.
9    // It should also be able to use saved servers nodes with their adress to easily publish to known nodes.
10
11    // connect to node
12    let registry = NodeRegistry::load()?;
13    let node_address = registry
14        .resolve_address(&node_ref)
15        .ok_or_else(|| IntersticeError::Internal("Unknown node".into()))?;
16    let (mut stream, _handshake) = handshake_with_node(&node_address).await?;
17
18    // Build module using cargo
19    println!(
20        "Building module from {} using cargo...",
21        module_project_path.display()
22    );
23    let manifest_path = module_project_path.join("Cargo.toml");
24    let output = std::process::Command::new("cargo")
25        .args([
26            "build",
27            "--release",
28            "--target",
29            "wasm32-unknown-unknown",
30            "--manifest-path",
31            manifest_path.to_string_lossy().as_ref(),
32        ])
33        .output()
34        .expect("Failed to execute cargo build command");
35    if !output.status.success() {
36        return Err(IntersticeError::Internal(format!(
37            "Cargo build failed: {}",
38            String::from_utf8_lossy(&output.stderr)
39        )));
40    }
41    // Read generated wasm file
42    let wasm_path = resolve_wasm_path(&manifest_path)?;
43    let wasm_binary = std::fs::read(wasm_path).expect("Failed to read generated wasm file");
44
45    // Send wasm binary to node
46    let packet = NetworkPacket::ModuleEvent(ModuleEventInstance::Publish { wasm_binary });
47    write_packet(&mut stream, &packet).await?;
48
49    // Close connection properly
50    let packet = NetworkPacket::Close;
51    write_packet(&mut stream, &packet).await?;
52
53    Ok(())
54}
55
56pub async fn remove(node_ref: String, module_name: &str) -> Result<(), IntersticeError> {
57    // This should take a module name and send a message to the node to delete the module with that name. It should also be able to use saved servers nodes with their adress to easily delete from known nodes.
58
59    // connect to node
60    let registry = NodeRegistry::load()?;
61    let node_address = registry
62        .resolve_address(&node_ref)
63        .ok_or_else(|| IntersticeError::Internal("Unknown node".into()))?;
64    let (mut stream, _handshake) = handshake_with_node(&node_address).await?;
65
66    // Send wasm binary to node
67    let packet = NetworkPacket::ModuleEvent(ModuleEventInstance::Remove {
68        module_name: module_name.into(),
69    });
70    write_packet(&mut stream, &packet).await?;
71
72    // Close connection properly
73    let packet = NetworkPacket::Close;
74    write_packet(&mut stream, &packet).await?;
75
76    Ok(())
77}
78
79fn resolve_wasm_path(manifest_path: &Path) -> Result<PathBuf, IntersticeError> {
80    let output = std::process::Command::new("cargo")
81        .args([
82            "metadata",
83            "--format-version",
84            "1",
85            "--no-deps",
86            "--manifest-path",
87            manifest_path.to_string_lossy().as_ref(),
88        ])
89        .output()
90        .map_err(|err| {
91            IntersticeError::Internal(format!("Failed to run cargo metadata: {}", err))
92        })?;
93
94    let metadata: Value = serde_json::from_slice(&output.stdout).map_err(|err| {
95        IntersticeError::Internal(format!("Failed to parse cargo metadata: {}", err))
96    })?;
97
98    let target_directory = metadata
99        .get("target_directory")
100        .and_then(|v| v.as_str())
101        .ok_or_else(|| IntersticeError::Internal("Missing target_directory".into()))?;
102
103    let packages = metadata
104        .get("packages")
105        .and_then(|v| v.as_array())
106        .ok_or_else(|| IntersticeError::Internal("Missing packages".into()))?;
107
108    let manifest_str = normalize_path(manifest_path)?;
109
110    let package = packages
111        .iter()
112        .find(|pkg| {
113            pkg.get("manifest_path")
114                .and_then(|v| v.as_str())
115                .and_then(|p| normalize_path_str(p).ok())
116                .map(|p| p == manifest_str)
117                .unwrap_or(false)
118        })
119        .ok_or_else(|| IntersticeError::Internal("Could not find package in metadata".into()))?;
120
121    let package_name = package
122        .get("name")
123        .and_then(|v| v.as_str())
124        .ok_or_else(|| IntersticeError::Internal("Missing package name".into()))?;
125
126    // Cargo converts hyphens to underscores in output filenames
127    let wasm_filename = package_name.replace('-', "_");
128
129    Ok(PathBuf::from(target_directory)
130        .join("wasm32-unknown-unknown")
131        .join("release")
132        .join(wasm_filename)
133        .with_extension("wasm"))
134}
135
136fn normalize_path(path: &Path) -> Result<String, IntersticeError> {
137    let canonical = path
138        .canonicalize()
139        .map_err(|err| IntersticeError::Internal(format!("Invalid manifest path: {}", err)))?;
140    Ok(normalize_path_buf(canonical))
141}
142
143fn normalize_path_str(path: &str) -> Result<String, IntersticeError> {
144    Ok(normalize_path_buf(PathBuf::from(path)))
145}
146
147fn normalize_path_buf(path: PathBuf) -> String {
148    let s = path.to_string_lossy().replace('\\', "/");
149    s.strip_prefix("//?/")
150        .or_else(|| s.strip_prefix("\\\\?\\"))
151        .unwrap_or(&s)
152        .to_string()
153}