ruxgo/
parser.rs

1//! Parsing Module
2
3use crate::builder::Target;
4use crate::utils::log::{log, LogLevel};
5use serde::Serialize;
6use std::collections::{HashMap, HashSet, VecDeque};
7use std::default::Default;
8use std::fs::File;
9use std::process::Command;
10use std::sync::{Arc, RwLock};
11use std::{io::Read, path::Path};
12use toml::{Table, Value};
13use walkdir::WalkDir;
14
15/// Struct descibing the build config of the local project
16#[derive(Debug, Clone)]
17pub struct BuildConfig {
18    pub compiler: Arc<RwLock<String>>,
19    pub app: String,
20}
21
22/// Struct descibing the OS config of the local project
23#[derive(Debug, Default, PartialEq, Clone, Serialize)]
24pub struct OSConfig {
25    pub name: String,
26    pub features: Vec<String>,
27    pub ulib: String,
28    pub develop: String,
29    pub platform: PlatformConfig,
30}
31
32/// Struct descibing the platform config of the local project
33#[derive(Debug, Default, PartialEq, Clone, Serialize)]
34pub struct PlatformConfig {
35    pub name: String,
36    pub arch: String,
37    pub cross_compile: String,
38    pub target: String,
39    pub smp: String,
40    pub mode: String,
41    pub log: String,
42    pub v: String,
43    pub qemu: QemuConfig,
44}
45
46/// Struct descibing the qemu config of the local project
47#[derive(Debug, Default, PartialEq, Clone, Serialize)]
48pub struct QemuConfig {
49    pub debug: String,
50    pub blk: String,
51    pub net: String,
52    pub mem: String,
53    pub graphic: String,
54    pub bus: String,
55    pub disk_img: String,
56    pub v9p: String,
57    pub v9p_path: String,
58    pub accel: String,
59    pub qemu_log: String,
60    pub net_dump: String,
61    pub net_dev: String,
62    pub ip: String,
63    pub gw: String,
64    pub args: String,
65    pub envs: String,
66}
67
68impl QemuConfig {
69    /// This function is used to config qemu parameters when running on qemu
70    pub fn config_qemu(
71        &self,
72        platform_config: &PlatformConfig,
73        trgt: &Target,
74    ) -> (Vec<String>, Vec<String>) {
75        // vdev_suffix
76        let vdev_suffix = match self.bus.as_str() {
77            "mmio" => "device",
78            "pci" => "pci",
79            _ => {
80                log(LogLevel::Error, "BUS must be one of 'mmio' or 'pci'");
81                std::process::exit(1);
82            }
83        };
84        // config qemu
85        let mut qemu_args = Vec::new();
86        qemu_args.push(format!("qemu-system-{}", platform_config.arch));
87        // init
88        qemu_args.push("-m".to_string());
89        qemu_args.push(self.mem.to_string());
90        qemu_args.push("-smp".to_string());
91        qemu_args.push(platform_config.smp.clone());
92        // arch
93        match platform_config.arch.as_str() {
94            "x86_64" => {
95                qemu_args.extend(
96                    ["-machine", "q35", "-kernel", &trgt.elf_path]
97                        .iter()
98                        .map(|&arg| arg.to_string()),
99                );
100            }
101            "risc64" => {
102                qemu_args.extend(
103                    [
104                        "-machine",
105                        "virt",
106                        "-bios",
107                        "default",
108                        "-kernel",
109                        &trgt.bin_path,
110                    ]
111                    .iter()
112                    .map(|&arg| arg.to_string()),
113                );
114            }
115            "aarch64" => {
116                qemu_args.extend(
117                    [
118                        "-cpu",
119                        "cortex-a72",
120                        "-machine",
121                        "virt",
122                        "-kernel",
123                        &trgt.bin_path,
124                    ]
125                    .iter()
126                    .map(|&arg| arg.to_string()),
127                );
128            }
129            _ => {
130                log(LogLevel::Error, "Unsupported architecture");
131                std::process::exit(1);
132            }
133        };
134        // args and envs
135        qemu_args.push("-append".to_string());
136        qemu_args.push(format!("\";{};{}\"", self.args, self.envs));
137        // blk
138        if self.blk == "y" {
139            qemu_args.push("-device".to_string());
140            qemu_args.push(format!("virtio-blk-{},drive=disk0", vdev_suffix));
141            qemu_args.push("-drive".to_string());
142            qemu_args.push(format!(
143                "id=disk0,if=none,format=raw,file={}",
144                self.disk_img
145            ));
146        }
147        // v9p
148        if self.v9p == "y" {
149            qemu_args.push("-fsdev".to_string());
150            qemu_args.push(format!(
151                "local,id=myid,path={},security_model=none",
152                self.v9p_path
153            ));
154            qemu_args.push("-device".to_string());
155            qemu_args.push(format!(
156                "virtio-9p-{},fsdev=myid,mount_tag=rootfs",
157                vdev_suffix
158            ));
159        }
160        // net
161        if self.net == "y" {
162            qemu_args.push("-device".to_string());
163            qemu_args.push(format!("virtio-net-{},netdev=net0", vdev_suffix));
164            // net_dev
165            if self.net_dev == "user" {
166                qemu_args.push("-netdev".to_string());
167                qemu_args.push(
168                    "user,id=net0,hostfwd=tcp::5555-:5555,hostfwd=udp::5555-:5555".to_string(),
169                );
170            } else if self.net_dev == "tap" {
171                qemu_args.push("-netdev".to_string());
172                qemu_args.push("tap,id=net0,ifname=tap0,script=no,downscript=no".to_string());
173            } else {
174                log(LogLevel::Error, "NET_DEV must be one of 'user' or 'tap'");
175                std::process::exit(1);
176            }
177            // net_dump
178            if self.net_dump == "y" {
179                qemu_args.push("-object".to_string());
180                qemu_args.push("filter-dump,id=dump0,netdev=net0,file=netdump.pcap".to_string());
181            }
182        }
183        // graphic
184        if self.graphic == "y" {
185            qemu_args.push("-device".to_string());
186            qemu_args.push(format!("virtio-gpu-{}", vdev_suffix));
187            qemu_args.push("-vga".to_string());
188            qemu_args.push("none".to_string());
189            qemu_args.push("-serial".to_string());
190            qemu_args.push("mon:stdio".to_string());
191        } else if self.graphic == "n" {
192            qemu_args.push("-nographic".to_string());
193        }
194        // qemu_log
195        if self.qemu_log == "y" {
196            qemu_args.push("-D".to_string());
197            qemu_args.push("qemu.log".to_string());
198            qemu_args.push("-d".to_string());
199            qemu_args.push("in_asm,int,mmu,pcall,cpu_reset,guest_errors".to_string());
200        }
201        // debug
202        let mut qemu_args_debug = Vec::new();
203        qemu_args_debug.extend(qemu_args.clone());
204        qemu_args_debug.push("-s".to_string());
205        qemu_args_debug.push("-S".to_string());
206        // acceel
207        if self.accel == "y" {
208            if cfg!(target_os = "darwin") {
209                qemu_args.push("-cpu".to_string());
210                qemu_args.push("host".to_string());
211                qemu_args.push("-accel".to_string());
212                qemu_args.push("hvf".to_string());
213            } else {
214                qemu_args.push("-cpu".to_string());
215                qemu_args.push("host".to_string());
216                qemu_args.push("-accel".to_string());
217                qemu_args.push("kvm".to_string());
218            }
219        }
220
221        (qemu_args, qemu_args_debug)
222    }
223}
224
225/// Struct describing the target config of the local project
226#[derive(Debug, Clone)]
227pub struct TargetConfig {
228    pub name: String,
229    pub src: String,
230    pub src_only: Vec<String>,
231    pub src_exclude: Vec<String>,
232    pub include_dir: Vec<String>,
233    pub typ: String,
234    pub cflags: String,
235    pub archive: String,
236    pub linker: String,
237    pub ldflags: String,
238    pub deps: Vec<String>,
239}
240
241impl TargetConfig {
242    /// Returns a vec of all filenames ending in .cpp or .c in the src directory
243    /// # Arguments
244    /// * `path` - The path to the src directory
245    fn get_src_names(&self, tgt_path: &str) -> Vec<String> {
246        let mut src_names = Vec::new();
247        let src_path = Path::new(tgt_path);
248
249        let walker = WalkDir::new(src_path)
250            .into_iter()
251            .filter_entry(|e| self.should_include(e.path()) && !self.should_exclude(e.path()));
252        for entry in walker.filter_map(|e| e.ok()) {
253            let path = entry.path();
254            if path.is_file() {
255                if let Some(ext) = path.extension() {
256                    if ext == "cpp" || ext == "c" {
257                        if let Some(file_path_str) = path.to_str() {
258                            #[cfg(target_os = "windows")]
259                            let formatted_path_str = file_path_str.replace('\\', "/");
260                            #[cfg(target_os = "linux")]
261                            let formatted_path_str = file_path_str.to_string();
262                            src_names.push(formatted_path_str);
263                        }
264                    }
265                }
266            }
267        }
268
269        src_names
270    }
271
272    /// Exclusion logic: Check if the path is in src_exclude
273    fn should_exclude(&self, path: &Path) -> bool {
274        self.src_exclude
275            .iter()
276            .any(|excluded| path.to_str().map_or(false, |p| p.contains(excluded)))
277    }
278
279    /// Inclusion logic: Apply src_only logic only to files
280    fn should_include(&self, path: &Path) -> bool {
281        if self.src_only.is_empty() {
282            return true;
283        }
284        self.src_only
285            .iter()
286            .any(|included| path.to_str().map_or(false, |p| p.contains(included)))
287    }
288
289    /// Checks for duplicate source files in the target
290    fn check_duplicate_srcs(&self) {
291        let mut src_file_names = self.get_src_names(&self.src);
292        src_file_names.sort_unstable();
293        src_file_names.dedup();
294        let mut last_name: Option<String> = None;
295        let mut duplicates = Vec::new();
296
297        for file_name in &src_file_names {
298            if let Some(ref last) = last_name {
299                if last == file_name {
300                    duplicates.push(file_name.clone());
301                }
302            }
303            last_name = Some(file_name.clone());
304        }
305        if !duplicates.is_empty() {
306            log(
307                LogLevel::Error,
308                &format!("Duplicate source files found for target: {}", self.name),
309            );
310            log(LogLevel::Error, "Source files must be unique");
311            for duplicate in duplicates {
312                log(LogLevel::Error, &format!("Duplicate file: {}", duplicate));
313            }
314            std::process::exit(1);
315        }
316    }
317
318    /// Rearrange the input targets
319    /// Using topological sorting to respect dependencies.
320    fn arrange_targets(targets: Vec<TargetConfig>) -> Vec<TargetConfig> {
321        // Create a mapping from the target name to the target configuration
322        let mut target_map: HashMap<String, TargetConfig> = targets
323            .into_iter()
324            .map(|target| (target.name.clone(), target))
325            .collect();
326
327        // Build a graph and an in-degree table,and initialize
328        let mut graph: HashMap<String, Vec<String>> = HashMap::new();
329        let mut in_degree: HashMap<String, usize> = HashMap::new();
330        for name in target_map.keys() {
331            graph.entry(name.clone()).or_default();
332            in_degree.entry(name.clone()).or_insert(0);
333        }
334
335        // Fill the graph and update the in-degree table
336        for target in target_map.values() {
337            for dep in &target.deps {
338                if let Some(deps) = graph.get_mut(dep) {
339                    deps.push(target.name.clone());
340                    *in_degree.entry(target.name.clone()).or_insert(0) += 1;
341                }
342            }
343        }
344
345        // Using topological sort
346        let mut queue: VecDeque<String> = VecDeque::new();
347        for (name, &degree) in &in_degree {
348            if degree == 0 {
349                queue.push_back(name.clone());
350            }
351        }
352        let mut sorted_names = Vec::new();
353        while let Some(name) = queue.pop_front() {
354            sorted_names.push(name.clone());
355            if let Some(deps) = graph.get(&name) {
356                for dep in deps {
357                    let degree = in_degree.entry(dep.clone()).or_default();
358                    *degree -= 1;
359                    if *degree == 0 {
360                        queue.push_back(dep.clone());
361                    }
362                }
363            }
364        }
365
366        // Check for rings
367        if sorted_names.len() != target_map.len() {
368            log(LogLevel::Error, "Circular dependency detected");
369            std::process::exit(1);
370        }
371
372        // Rebuild the target list based on the sorted names
373        sorted_names
374            .into_iter()
375            .map(|name| target_map.remove(&name).unwrap())
376            .collect()
377    }
378}
379
380/// This function is used to parse the config file of local project
381/// # Arguments
382/// * `path` - The path to the config file
383/// * `check_dup_src` - If true, the function will check for duplicately named source files
384pub fn parse_config(path: &str, check_dup_src: bool) -> (BuildConfig, OSConfig, Vec<TargetConfig>) {
385    // Open toml file and parse it into a string
386    let mut file = File::open(path).unwrap_or_else(|_| {
387        log(
388            LogLevel::Error,
389            &format!("Could not open config file: {}", path),
390        );
391        std::process::exit(1);
392    });
393    let mut contents = String::new();
394    file.read_to_string(&mut contents).unwrap_or_else(|_| {
395        log(
396            LogLevel::Error,
397            &format!("Could not read config file: {}", path),
398        );
399        std::process::exit(1);
400    });
401    let config = contents.parse::<Table>().unwrap_or_else(|e| {
402        log(
403            LogLevel::Error,
404            &format!("Could not parse config file: {}", path),
405        );
406        log(LogLevel::Error, &format!("Error: {}", e));
407        std::process::exit(1);
408    });
409
410    let build_config = parse_build_config(&config);
411    let os_config = parse_os_config(&config, &build_config);
412    let targets = parse_targets(&config, &build_config, check_dup_src);
413
414    (build_config, os_config, targets)
415}
416
417/// Parses the build configuration
418fn parse_build_config(config: &Table) -> BuildConfig {
419    let build = config["build"].as_table().unwrap_or_else(|| {
420        log(LogLevel::Error, "Could not find build in config file");
421        std::process::exit(1);
422    });
423    let compiler = Arc::new(RwLock::new(parse_cfg_string(build, "compiler", "")));
424    let app = parse_cfg_string(build, "app", "");
425
426    BuildConfig { compiler, app }
427}
428
429/// Parses the OS configuration
430fn parse_os_config(config: &Table, build_config: &BuildConfig) -> OSConfig {
431    let empty_os = Value::Table(toml::map::Map::default());
432    let os = config.get("os").unwrap_or(&empty_os);
433    let os_config: OSConfig;
434    if os != &empty_os {
435        if let Some(os_table) = os.as_table() {
436            let name = parse_cfg_string(os_table, "name", "");
437            let ulib = parse_cfg_string(os_table, "ulib", "");
438            let develop = parse_cfg_string(os_table, "develop", "n");
439            let mut features = parse_cfg_vector(os_table, "services");
440            if features.iter().any(|feat| {
441                feat == "fs"
442                    || feat == "net"
443                    || feat == "pipe"
444                    || feat == "select"
445                    || feat == "poll"
446                    || feat == "epoll"
447            }) {
448                features.push("fd".to_string());
449            }
450            if ulib == "ruxmusl" {
451                features.push("musl".to_string());
452                features.push("fp_simd".to_string());
453                features.push("fd".to_string());
454                features.push("tls".to_string());
455            }
456            // Parse platform (if empty, it is the default value)
457            let platform = parse_platform(os_table);
458            let current_compiler = build_config.compiler.read().unwrap();
459            let new_compiler = format!("{}{}", platform.cross_compile, *current_compiler);
460            drop(current_compiler);
461            *build_config.compiler.write().unwrap() = new_compiler;
462            os_config = OSConfig {
463                name,
464                features,
465                ulib,
466                develop,
467                platform,
468            };
469        } else {
470            log(LogLevel::Error, "OS is not a table");
471            std::process::exit(1);
472        }
473    } else {
474        os_config = OSConfig::default();
475    }
476
477    os_config
478}
479
480/// Parses the targets configuration
481fn parse_targets(
482    config: &Table,
483    build_config: &BuildConfig,
484    check_dup_src: bool,
485) -> Vec<TargetConfig> {
486    let mut tgts = Vec::new();
487    let targets = config.get("targets").and_then(|v| v.as_array());
488    if targets.is_none() && build_config.app.is_empty() {
489        log(
490            LogLevel::Error,
491            "Could not find targets or app in config file",
492        );
493        std::process::exit(1);
494    }
495    if let Some(targets) = targets {
496        for target in targets {
497            let target_tb = target.as_table().unwrap_or_else(|| {
498                log(LogLevel::Error, "Target is not a table");
499                std::process::exit(1);
500            });
501            // include_dir is compatible with both string and vector types
502            let include_dir = if let Some(value) = target_tb.get("include_dir") {
503                match value {
504                    Value::String(_s) => vec![parse_cfg_string(target_tb, "include_dir", "./")],
505                    Value::Array(_arr) => parse_cfg_vector(target_tb, "include_dir"),
506                    _ => {
507                        log(LogLevel::Error, "Invalid include_dir field");
508                        std::process::exit(1);
509                    }
510                }
511            } else {
512                vec!["./".to_owned()]
513            };
514            let target_config = TargetConfig {
515                name: parse_cfg_string(target_tb, "name", ""),
516                src: parse_cfg_string(target_tb, "src", ""),
517                src_only: parse_cfg_vector(target_tb, "src_only"),
518                src_exclude: parse_cfg_vector(target_tb, "src_exclude"),
519                include_dir,
520                typ: parse_cfg_string(target_tb, "type", ""),
521                cflags: parse_cfg_string(target_tb, "cflags", ""),
522                archive: parse_cfg_string(target_tb, "archive", ""),
523                linker: parse_cfg_string(target_tb, "linker", ""),
524                ldflags: parse_cfg_string(target_tb, "ldflags", ""),
525                deps: parse_cfg_vector(target_tb, "deps"),
526            };
527            if target_config.typ != "exe"
528                && target_config.typ != "dll"
529                && target_config.typ != "static"
530                && target_config.typ != "object"
531            {
532                log(LogLevel::Error, "Type must be exe, dll, object or static");
533                std::process::exit(1);
534            }
535            tgts.push(target_config);
536        }
537    }
538
539    if tgts.is_empty() && build_config.app.is_empty() {
540        log(LogLevel::Error, "No targets found!");
541        std::process::exit(1);
542    }
543
544    // Checks for duplicate target names
545    let mut names_set = HashSet::new();
546    for target in &tgts {
547        if !names_set.insert(&target.name) {
548            log(
549                LogLevel::Error,
550                &format!("Duplicate target names found: {}", target.name),
551            );
552            std::process::exit(1);
553        }
554    }
555
556    // Checks for duplicate srcs in the target
557    if check_dup_src {
558        log(
559            LogLevel::Info,
560            "Checking for duplicate srcs in all targets...",
561        );
562        for target in &tgts {
563            target.check_duplicate_srcs();
564        }
565    }
566
567    TargetConfig::arrange_targets(tgts)
568}
569
570/// Parses the platform configuration
571fn parse_platform(config: &Table) -> PlatformConfig {
572    let empty_platform = Value::Table(toml::map::Map::default());
573    let platform = config.get("platform").unwrap_or(&empty_platform);
574    if let Some(platform_table) = platform.as_table() {
575        let name = parse_cfg_string(platform_table, "name", "x86_64-qemu-q35");
576        let arch = name.split('-').next().unwrap_or("x86_64").to_string();
577        let cross_compile = format!("{}-linux-musl-", arch);
578        let target = match &arch[..] {
579            "x86_64" => "x86_64-unknown-none".to_string(),
580            "riscv64" => "riscv64gc-unknown-none-elf".to_string(),
581            "aarch64" => "aarch64-unknown-none-softfloat".to_string(),
582            _ => {
583                log(
584                    LogLevel::Error,
585                    "\"ARCH\" must be one of \"x86_64\", \"riscv64\", or \"aarch64\"",
586                );
587                std::process::exit(1);
588            }
589        };
590        let smp = parse_cfg_string(platform_table, "smp", "1");
591        let mode = parse_cfg_string(platform_table, "mode", "");
592        let log = parse_cfg_string(platform_table, "log", "warn");
593        let v = parse_cfg_string(platform_table, "v", "");
594        // determine whether enable qemu
595        let qemu: QemuConfig = if name.split('-').any(|s| s == "qemu") {
596            parse_qemu(&arch, platform_table)
597        } else {
598            QemuConfig::default()
599        };
600        PlatformConfig {
601            name,
602            arch,
603            cross_compile,
604            target,
605            smp,
606            mode,
607            log,
608            v,
609            qemu,
610        }
611    } else {
612        log(LogLevel::Error, "Platform is not a table");
613        std::process::exit(1);
614    }
615}
616
617/// Parses the qemu configuration
618fn parse_qemu(arch: &str, config: &Table) -> QemuConfig {
619    let empty_qemu = Value::Table(toml::map::Map::default());
620    let qemu = config.get("qemu").unwrap_or(&empty_qemu);
621    if let Some(qemu_table) = qemu.as_table() {
622        let debug = parse_cfg_string(qemu_table, "debug", "n");
623        let blk = parse_cfg_string(qemu_table, "blk", "n");
624        let net = parse_cfg_string(qemu_table, "net", "n");
625        let mem = parse_cfg_string(qemu_table, "memory", "128M");
626        let graphic = parse_cfg_string(qemu_table, "graphic", "n");
627        let bus = match arch {
628            "x86_64" => "pci".to_string(),
629            _ => "mmio".to_string(),
630        };
631        let disk_img = parse_cfg_string(qemu_table, "disk_img", "disk.img");
632        let v9p = parse_cfg_string(qemu_table, "v9p", "n");
633        let v9p_path = parse_cfg_string(qemu_table, "v9p_path", "./");
634        let accel_pre = match Command::new("uname").arg("-r").output() {
635            Ok(output) => {
636                let kernel_version = String::from_utf8_lossy(&output.stdout).to_lowercase();
637                if kernel_version.contains("-microsoft") {
638                    "n"
639                } else {
640                    "y"
641                }
642            }
643            Err(_) => {
644                log(LogLevel::Error, "Failed to execute command");
645                std::process::exit(1);
646            }
647        };
648        let accel = match arch {
649            "x86_64" => accel_pre.to_string(),
650            _ => "n".to_string(),
651        };
652        let qemu_log = parse_cfg_string(qemu_table, "qemu_log", "n");
653        let net_dump = parse_cfg_string(qemu_table, "net_dump", "n");
654        let net_dev = parse_cfg_string(qemu_table, "net_dev", "user");
655        let ip = parse_cfg_string(qemu_table, "ip", "10.0.2.15");
656        let gw = parse_cfg_string(qemu_table, "gw", "10.0.2.2");
657        let args = parse_cfg_string(qemu_table, "args", "");
658        let envs = parse_cfg_string(qemu_table, "envs", "");
659        QemuConfig {
660            debug,
661            blk,
662            net,
663            mem,
664            graphic,
665            bus,
666            disk_img,
667            v9p,
668            v9p_path,
669            accel,
670            qemu_log,
671            net_dump,
672            net_dev,
673            ip,
674            gw,
675            args,
676            envs,
677        }
678    } else {
679        log(LogLevel::Error, "Qemu is not a table");
680        std::process::exit(1);
681    }
682}
683
684/// Parses the configuration field of the string type
685fn parse_cfg_string(config: &Table, field: &str, default: &str) -> String {
686    let default_string = Value::String(default.to_string());
687    config
688        .get(field)
689        .unwrap_or(&default_string)
690        .as_str()
691        .unwrap_or_else(|| {
692            log(LogLevel::Error, &format!("{} is not a string", field));
693            std::process::exit(1);
694        })
695        .to_string()
696}
697
698/// Parses the configuration field of the vector type
699fn parse_cfg_vector(config: &Table, field: &str) -> Vec<String> {
700    let empty_vector = Value::Array(Vec::new());
701    config
702        .get(field)
703        .unwrap_or(&empty_vector)
704        .as_array()
705        .unwrap_or_else(|| {
706            log(LogLevel::Error, &format!("{} is not an array", field));
707            std::process::exit(1);
708        })
709        .iter()
710        .map(|value| {
711            value
712                .as_str()
713                .unwrap_or_else(|| {
714                    log(LogLevel::Error, &format!("{} elements are strings", field));
715                    std::process::exit(1);
716                })
717                .to_string()
718        })
719        .collect()
720}