diff --git a/backend/parsers/windmill-parser-csharp/src/lib.rs b/backend/parsers/windmill-parser-csharp/src/lib.rs index 5c42bb3624..b80f4c22bb 100644 --- a/backend/parsers/windmill-parser-csharp/src/lib.rs +++ b/backend/parsers/windmill-parser-csharp/src/lib.rs @@ -32,7 +32,7 @@ pub fn parse_csharp_signature(code: &str) -> anyhow::Result { if let Some(param_list) = sig.child_by_field_name("parameters") { for c in param_list.children(&mut param_list.walk()) { if c.kind() == "parameter" { - let (otyp, typ, name) = parse_csharp_typ(c, code); + let (otyp, typ, name) = parse_csharp_typ(c, code)?; args.push(Arg { name, otyp, @@ -41,19 +41,8 @@ pub fn parse_csharp_signature(code: &str) -> anyhow::Result { has_default: false, oidx: None, }); - for (i, w) in c.children(&mut c.walk()).enumerate() { - let s = w.utf8_text(code.as_bytes()); - println!( - " {:?} - {:?} - {:?}", - w, - c.field_name_for_child((i) as u32), - s - ); - } } } - } else { - println!("No one with parameter_list"); } } @@ -66,7 +55,46 @@ pub fn parse_csharp_signature(code: &str) -> anyhow::Result { }) } -fn parse_csharp_typ<'a>(param_node: Node<'a>, code: &str) -> (Option, Typ, String) { +fn find_typ<'a>(typ_node: Node<'a>, code: &str) -> anyhow::Result { + match typ_node.kind() { + "predefined_type" => { + match typ_node.utf8_text(code.as_bytes()) { + Ok("string") => Ok(Typ::Str(None)), + Ok("sbyte") | Ok("System.SByte") => Ok(Typ::Bytes), + Ok("byte") | Ok("System.Byte") => Ok(Typ::Bytes), + Ok("short") | Ok("System.Int16") => Ok(Typ::Int), + Ok("ushort") | Ok("System.UInt16") => Ok(Typ::Int), + Ok("int") | Ok("System.Int32") => Ok(Typ::Int), + Ok("uint") | Ok("System.UInt32") => Ok(Typ::Int), + Ok("long") | Ok("System.Int64") => Ok(Typ::Int), + Ok("ulong") | Ok("System.UInt64") => Ok(Typ::Int), + Ok("char") | Ok("System.Char") => Ok(Typ::Str(None)), + Ok("float") | Ok("System.Single") => Ok(Typ::Float), + Ok("double") | Ok("System.Double") => Ok(Typ::Float), + Ok("bool") | Ok("System.Boolean") => Ok(Typ::Bool), + Ok("decimal") | Ok("System.Decimal") => Ok(Typ::Float), + Ok("object") => Ok(Typ::Object(vec![])), // TODO: Complete the object type + Ok(s) => Err(anyhow!("Unknown type `{s}`")), + Err(e) => Err(anyhow!("Error getting type name: {}", e)), + } + } + "array_type" => { + let new_typ_node = typ_node + .child_by_field_name("type") + .ok_or(anyhow!("Failed to find inner type of array type"))?; + Ok(Typ::List(Box::new(find_typ(new_typ_node, code)?))) + } + "identifier" => Ok(Typ::Unknown), + "generic_name" => Ok(Typ::Unknown), + "pointer_type" => Ok(Typ::Int), + wc => Err(anyhow!("Unexpected node kind: {}", wc)), + } +} + +fn parse_csharp_typ<'a>( + param_node: Node<'a>, + code: &str, +) -> anyhow::Result<(Option, Typ, String)> { let name = param_node .child_by_field_name("name") .and_then(|n| n.utf8_text(code.as_bytes()).ok()) @@ -76,9 +104,9 @@ fn parse_csharp_typ<'a>(param_node: Node<'a>, code: &str) -> (Option, Ty .and_then(|n| n.utf8_text(code.as_bytes()).ok()) .map(|s| s.to_string()); - let typ = Typ::Str(None); + let typ = find_typ(otyp_node.unwrap(), code)?; - (otyp, typ, name.to_string()) + Ok((otyp, typ, name.to_string())) } // Function to find the Main method's signature @@ -108,6 +136,39 @@ fn find_main_signature<'a>(root_node: Node<'a>, code: &str) -> Option> return None; } +pub fn parse_csharp_reqs(code: &str) -> (Vec<(String, Option)>, Vec) { + let mut nuget_reqs = Vec::new(); + let mut pkg_lines = Vec::new(); + + for (i, line) in code.split("\n").enumerate() { + if line.starts_with('#') { + if let Some(req) = parse_nuget_req(&line) { + pkg_lines.push(i); + nuget_reqs.push(req); + } + } else { + break; // Stop processing after the first non-comment line + } + } + + (nuget_reqs, pkg_lines) +} + +fn parse_nuget_req(line: &str) -> Option<(String, Option)> { + // Check if the line starts with `#r "nuget:` + if let Some(start) = line.find("#r \"nuget:") { + // Extract the content after `#r "nuget:` + let start_idx = start + 10; + let end_idx = line[start_idx..].find('"')?; + let line = &line[start_idx..start_idx + end_idx]; + let mut splitted = line.split(","); + if let Some(pkg) = splitted.next() { + return Some((pkg.trim().to_string(), splitted.next().map(|s| s.trim().to_string()))); + } + } + None +} + #[cfg(test)] mod test { @@ -120,7 +181,7 @@ using System; class LilProgram { - public static string Main(string myString = "World", int myInt) + public static string Main(string myString = "World", int myInt, string[] jj) { Console.Writeline("Hello!!"); return "yeah"; @@ -129,7 +190,7 @@ class LilProgram }"#; let ret = parse_csharp_signature(code).unwrap(); - assert_eq!(ret.args.len(), 2); + assert_eq!(ret.args.len(), 3); assert_eq!(ret.args[0].name, "myString"); assert_eq!(ret.args[0].otyp, Some("string".to_string())); @@ -138,5 +199,28 @@ class LilProgram assert_eq!(ret.args[1].name, "myInt"); assert_eq!(ret.args[1].otyp, Some("int".to_string())); assert_eq!(ret.args[1].typ, Typ::Int); + + assert_eq!(ret.args[2].name, "jj"); + assert_eq!(ret.args[2].otyp, Some("string[]".to_string())); + assert_eq!(ret.args[2].typ, Typ::List(Box::new(Typ::Str(None)))); + } + + #[test] + fn test_parse_csharp_reqs() { + let file_content = r#"#r "nuget: AutoMapper, 6.1.0" +#r "nuget: Newtonsoft.Json, 13.0.1" +#r "nuget: Serilog, 2.10.0" +# This is a comment +#r "nuget: Serilog, 2.10.0" + +using System; +"#; + + let requirements = parse_csharp_reqs(file_content).0; + + assert_eq!(requirements.len(), 3); + assert_eq!(requirements[0], ("AutoMapper".to_string(), Some("6.1.0".to_string()))); + assert_eq!(requirements[1], ("Newtonsoft.Json".to_string(), Some("13.0.1".to_string()))); + assert_eq!(requirements[2], ("Serilog".to_string(), Some("2.10.0".to_string()))); } } diff --git a/backend/src/main.rs b/backend/src/main.rs index 3d509cedaf..b9281385f9 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -66,11 +66,10 @@ use windmill_common::global_settings::OBJECT_STORE_CACHE_CONFIG_SETTING; use windmill_worker::{ get_hub_script_content_and_requirements, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR, - BUN_DEPSTAR_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM, - GO_BIN_CACHE_DIR, GO_CACHE_DIR, LOCK_CACHE_DIR, PIP_CACHE_DIR, POWERSHELL_CACHE_DIR, - PY311_CACHE_DIR, CSHARP_CACHE_DIR, RUST_CACHE_DIR, TAR_PIP_CACHE_DIR, TAR_PY311_CACHE_DIR, TMP_LOGS_DIR, - UV_CACHE_DIR, - RUST_CACHE_DIR, CSHARP_CACHE_DIR, TAR_PIP_CACHE_DIR, TMP_LOGS_DIR, UV_CACHE_DIR, + BUN_DEPSTAR_CACHE_DIR, CSHARP_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, + DENO_CACHE_DIR_NPM, GO_BIN_CACHE_DIR, GO_CACHE_DIR, LOCK_CACHE_DIR, PIP_CACHE_DIR, + POWERSHELL_CACHE_DIR, PY311_CACHE_DIR, RUST_CACHE_DIR, TAR_PIP_CACHE_DIR, TAR_PY311_CACHE_DIR, + TMP_LOGS_DIR, UV_CACHE_DIR, }; use crate::monitor::{ diff --git a/backend/windmill-worker/src/csharp_executor.rs b/backend/windmill-worker/src/csharp_executor.rs index 863e21bd91..7ee489bf1c 100644 --- a/backend/windmill-worker/src/csharp_executor.rs +++ b/backend/windmill-worker/src/csharp_executor.rs @@ -2,6 +2,7 @@ use anyhow::anyhow; use serde_json::value::RawValue; use std::{collections::HashMap, path::Path, process::Stdio}; use uuid::Uuid; +use windmill_parser_csharp::parse_csharp_reqs; use windmill_parser_rust::parse_rust_deps_into_manifest; use itertools::Itertools; @@ -33,22 +34,39 @@ lazy_static::lazy_static! { const CSHARP_OBJECT_STORE_PREFIX: &str = "csharpbin/"; -fn gen_cs_proj(code: &str, job_dir: &str) -> anyhow::Result<()> { +fn gen_cs_proj( + code: &str, + job_dir: &str, + reqs: Vec<(String, Option)>, +) -> anyhow::Result<()> { + let pkgs = reqs + .into_iter() + .map(|(pkg, vrsion_o)| { + let version = vrsion_o + .map(|v| format!("Version=\"{v}\"")) + .unwrap_or("".to_string()); + format!(" ") + }) + .join("\n"); + write_file( job_dir, "Main.csproj", - r#" - + &format!( + r#" Exe net7.0 enable - enable WindmillScriptCSharpInternal.Wrapper + +{pkgs} + -"#, +"# + ), )?; write_file(job_dir, "Script.cs", code)?; @@ -66,7 +84,8 @@ fn gen_cs_proj(code: &str, job_dir: &str) -> anyhow::Result<()> { .map(|x| { Ok(format!( " public {} {} {{ get; set; }}", - x.otyp.ok_or(anyhow!("Type not found for argument {}", x.name))?, + x.otyp + .ok_or(anyhow!("Type not found for argument {}", x.name))?, &x.name, )) }) @@ -173,17 +192,6 @@ async fn build_cs_proj( .await?; append_logs(job_id, w_id, "\n\n", db).await; - for entry in std::fs::read_dir(job_dir)? { - let entry = entry?; - let path = entry.path(); - - // Print file or directory name - if let Some(name) = path.file_name() { - // println!("{}", name.to_string_lossy()); - println!("{path:?}"); - } - } - let bin_path = format!("{}/{hash}", CSHARP_CACHE_DIR); match save_cache( @@ -202,6 +210,18 @@ async fn build_cs_proj( } } +fn remove_lines_from_text(contents: &str, indices_to_remove: Vec) -> String { + let mut result = Vec::new(); + + for (i, line) in contents.lines().enumerate() { + if !indices_to_remove.contains(&i) { + result.push(line); + } + } + + result.join("\n") +} + pub async fn handle_csharp_job( mem_peak: &mut i32, canceled_by: &mut Option, @@ -219,8 +239,6 @@ pub async fn handle_csharp_job( ) -> Result, Error> { check_executor_binary_exists("dotnet", DOTNET_PATH.as_str(), "C#")?; - - let hash = calculate_hash(&format!( "{}{}", inner_content, @@ -253,7 +271,26 @@ pub async fn handle_csharp_job( let logs1 = format!("{cache_logs}\n\n--- DOTNET BUILD ---\n"); append_logs(&job.id, &job.workspace_id, logs1, db).await; - gen_cs_proj(inner_content, job_dir)?; + let (reqs, lines_to_remove) = parse_csharp_reqs(inner_content); + for req in &reqs { + append_logs( + &job.id, + &job.workspace_id, + format!( + "Requirement detected: {} {}\n", + req.0, + req.1.as_ref().unwrap_or(&"".to_string()) + ), + db, + ) + .await; + } + + let inner_content = remove_lines_from_text(inner_content, lines_to_remove); + let code = inner_content.as_str(); + + + gen_cs_proj(code, job_dir, reqs)?; build_cs_proj( &job.id,