Skip to main content

cargo_faasta/
run.rs

1use anyhow::{Result, anyhow};
2use bitrpc::{RpcError, cyper::CyperTransport};
3use faasta_interface::{FunctionResult, FunctionServiceRpcClient};
4use std::io;
5use std::path::{Path as StdPath, PathBuf};
6use std::process::exit;
7use tracing::debug;
8use url::Url;
9
10/// Compare two file paths in a slightly more robust way.
11/// (On Windows, e.g., backslash vs forward slash).
12fn same_file_path(a: &str, b: &str) -> bool {
13    // Convert both to a canonical PathBuf
14    let path_a = StdPath::new(a).components().collect::<Vec<_>>();
15    let path_b = StdPath::new(b).components().collect::<Vec<_>>();
16    path_a == path_b
17}
18
19#[derive(Clone)]
20pub struct FunctionServiceClient {
21    endpoint: String,
22}
23
24impl FunctionServiceClient {
25    fn new(endpoint: String) -> Self {
26        Self { endpoint }
27    }
28
29    fn new_transport(&self) -> CyperTransport {
30        CyperTransport::new(self.endpoint.clone())
31    }
32
33    pub async fn publish(
34        &self,
35        wasm_file: Vec<u8>,
36        name: String,
37        github_auth_token: String,
38    ) -> Result<FunctionResult<String>, RpcError> {
39        let mut client = FunctionServiceRpcClient::new(self.new_transport());
40        let response = client.publish(wasm_file, name, github_auth_token).await?;
41        Ok(response)
42    }
43
44    pub async fn list_functions(
45        &self,
46        github_auth_token: String,
47    ) -> Result<FunctionResult<Vec<faasta_interface::FunctionInfo>>, RpcError> {
48        let mut client = FunctionServiceRpcClient::new(self.new_transport());
49        let response = client.list_functions(github_auth_token).await?;
50        Ok(response)
51    }
52
53    pub async fn unpublish(
54        &self,
55        name: String,
56        github_auth_token: String,
57    ) -> Result<FunctionResult<()>, RpcError> {
58        let mut client = FunctionServiceRpcClient::new(self.new_transport());
59        let response = client.unpublish(name, github_auth_token).await?;
60        Ok(response)
61    }
62
63    pub async fn get_metrics(
64        &self,
65        github_auth_token: String,
66    ) -> Result<FunctionResult<faasta_interface::Metrics>, RpcError> {
67        let mut client = FunctionServiceRpcClient::new(self.new_transport());
68        let response = client.get_metrics(github_auth_token).await?;
69        Ok(response)
70    }
71}
72
73fn normalize_endpoint(server_addr: &str) -> Result<String> {
74    let trimmed = server_addr.trim();
75    if trimmed.is_empty() {
76        return Err(anyhow!("Server address cannot be empty"));
77    }
78
79    let mut url = if trimmed.contains("://") {
80        Url::parse(trimmed).map_err(|e| anyhow!("Invalid server address '{trimmed}': {e}"))?
81    } else {
82        Url::parse(&format!("https://{trimmed}"))
83            .or_else(|_| Url::parse(&format!("https://{trimmed}/")))
84            .map_err(|e| anyhow!("Invalid server address '{trimmed}': {e}"))?
85    };
86
87    if url.scheme() != "https" {
88        url.set_scheme("https")
89            .map_err(|_| anyhow!("Server address must use HTTPS"))?;
90    }
91
92    if url.path() == "/" {
93        url.set_path("/rpc");
94    }
95
96    Ok(url.to_string())
97}
98
99// Create a connection to the function service
100pub async fn connect_to_function_service(server_addr: &str) -> Result<FunctionServiceClient> {
101    let endpoint = normalize_endpoint(server_addr)?;
102    debug!("Configured RPC endpoint: {}", endpoint);
103    Ok(FunctionServiceClient::new(endpoint))
104}
105
106/// Get the target directory and package name for the current project
107pub fn get_project_info() -> Result<(PathBuf, String, PathBuf), io::Error> {
108    let spinner = indicatif::ProgressBar::new_spinner();
109    spinner.set_message("Getting project information...");
110    spinner.enable_steady_tick(std::time::Duration::from_millis(100));
111
112    // Get package info using cargo metadata
113    let output = std::process::Command::new("cargo")
114        .args(["metadata", "--format-version=1"])
115        .output()
116        .unwrap_or_else(|e| {
117            spinner.finish_and_clear();
118            eprintln!("Failed to run cargo metadata: {e}");
119            exit(1);
120        });
121
122    if !output.status.success() {
123        spinner.finish_and_clear();
124        eprintln!("Failed to retrieve cargo metadata");
125        exit(1);
126    }
127
128    // Parse JSON
129    let metadata: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap_or_else(|e| {
130        spinner.finish_and_clear();
131        eprintln!("Failed to parse cargo metadata: {e}");
132        exit(1);
133    });
134
135    // Extract target_directory
136    let target_directory = metadata
137        .get("target_directory")
138        .and_then(serde_json::Value::as_str)
139        .map(PathBuf::from)
140        .unwrap_or_else(|| {
141            spinner.finish_and_clear();
142            eprintln!("No 'target_directory' found in cargo metadata");
143            exit(1);
144        });
145
146    // Get the package name from the current directory's Cargo.toml
147    let packages = metadata
148        .get("packages")
149        .and_then(serde_json::Value::as_array)
150        .unwrap_or_else(|| {
151            spinner.finish_and_clear();
152            eprintln!("No 'packages' found in cargo metadata");
153            exit(1);
154        });
155
156    // Find the package for the current directory
157    let current_dir = std::env::current_dir().unwrap_or_else(|e| {
158        spinner.finish_and_clear();
159        eprintln!("Failed to get current directory: {e}");
160        exit(1);
161    });
162
163    let package_name = packages
164        .iter()
165        .filter_map(|pkg| {
166            let manifest_path = pkg.get("manifest_path")?.as_str()?;
167            let pkg_dir = StdPath::new(manifest_path).parent()?;
168            if same_file_path(&pkg_dir.to_string_lossy(), &current_dir.to_string_lossy()) {
169                pkg.get("name")?.as_str().map(String::from)
170            } else {
171                None
172            }
173        })
174        .next()
175        .unwrap_or_else(|| {
176            spinner.finish_and_clear();
177            eprintln!("Could not find package for current directory");
178            exit(1);
179        });
180
181    spinner.finish_and_clear();
182    Ok((target_directory, package_name, current_dir))
183}
184
185/// Build the project for wasm32-wasip2 target
186pub fn build_project(package_root: &PathBuf) -> Result<(), io::Error> {
187    let spinner = indicatif::ProgressBar::new_spinner();
188    spinner.set_message("Building optimized WASI component...");
189    spinner.enable_steady_tick(std::time::Duration::from_millis(100));
190
191    // Validate the project structure
192    if !package_root.join("src").join("lib.rs").exists() {
193        spinner.finish_and_clear();
194        eprintln!("Error: src/lib.rs is missing. This file is required for Faasta functions.");
195        eprintln!("Hint: Run 'cargo faasta new <n>' to create a new Faasta project.");
196        exit(1);
197    }
198
199    // Build with wasm32-wasip2 target
200    let status = std::process::Command::new("cargo")
201        .args(["build", "--release", "--target", "wasm32-wasip2"])
202        .current_dir(package_root)
203        .status()
204        .unwrap_or_else(|e| {
205            spinner.finish_and_clear();
206            eprintln!("Failed to run cargo build: {e}");
207            exit(1);
208        });
209
210    if !status.success() {
211        spinner.finish_and_clear();
212        eprintln!("Build failed");
213        exit(1);
214    }
215
216    spinner.finish_and_clear();
217    println!("✅ Build successful!");
218    Ok(())
219}
220
221// The function to handle the run command
222pub async fn handle_run(port: u16) -> io::Result<()> {
223    // Get project information
224    let (target_directory, package_name, package_root) = get_project_info()?;
225
226    // Display project info
227    println!("Building project: {package_name}");
228    println!("Project root: {}", package_root.display());
229
230    // Build the project first
231    build_project(&package_root)?;
232
233    // Get the full WASM file path - use same logic as in deploy
234    let rust_compiled_name = package_name.replace('-', "_");
235    let wasm_filename = format!("{rust_compiled_name}.wasm");
236    let wasm_path = target_directory
237        .join("wasm32-wasip2")
238        .join("release")
239        .join(wasm_filename);
240
241    // Ensure the WASM file exists
242    if !wasm_path.exists() {
243        eprintln!(
244            "Error: Could not find compiled WASM at: {}",
245            wasm_path.display()
246        );
247        eprintln!("Build seems to have failed or produced output in a different location.");
248        exit(1);
249    }
250
251    println!("Starting local server on port {port}...");
252    let status = std::process::Command::new("wasmtime")
253        .args(["serve", &wasm_path.to_string_lossy()])
254        .current_dir(&package_root)
255        .status()
256        .unwrap_or_else(|e| {
257            eprintln!("Failed to run wasmtime serve: {e}");
258            exit(1);
259        });
260
261    if !status.success() {
262        eprintln!("wasmtime serve exited with an error");
263        exit(1);
264    }
265
266    Ok(())
267}