cargo_hax/
cargo_hax.rs

1#![feature(rustc_private)]
2use annotate_snippets::{Level, Renderer};
3use clap::Parser;
4use colored::Colorize;
5use hax_types::cli_options::*;
6use hax_types::driver_api::*;
7use hax_types::engine_api::*;
8use is_terminal::IsTerminal;
9use serde_jsonlines::BufReadExt;
10use std::collections::HashMap;
11use std::fs;
12use std::io::BufRead;
13use std::io::Write;
14use std::path::PathBuf;
15use std::process;
16
17mod engine_debug_webapp;
18use hax_frontend_exporter::id_table;
19
20/// Return a toolchain argument to pass to `cargo`: when the correct nightly is
21/// already present, this is None, otherwise we (1) ensure `rustup` is available
22/// (2) install the nightly (3) return the toolchain
23fn toolchain() -> Option<&'static str> {
24    let current_rustc_version = version_check::triple()
25        .map(|(_, channel, date)| format!("{channel}-{date}"))
26        .unwrap_or("unknown".into());
27    if env!("HAX_RUSTC_VERSION") != current_rustc_version {
28        const TOOLCHAIN: &str = env!("HAX_TOOLCHAIN");
29        // ensure rustup is available
30        which::which("rustup").ok().unwrap_or_else(|| {
31            println!("Error: {} was not found, but toolchain {} is required while the current toolchain is {}\n\nExiting.", "rustup".bold(), TOOLCHAIN.bold(), current_rustc_version.bold());
32            std::process::exit(1)
33        });
34        // make sure the toolchain is installed
35        rustup_toolchain::install(TOOLCHAIN).unwrap();
36        // return the correct toolchain
37        Some(TOOLCHAIN)
38    } else {
39        None
40    }
41}
42
43/// [`get_args`] is a wrapper of `std::env::args` that strips a possible
44/// cargo subcommand. This allows for a binary `BINARY` to be called
45/// both with `cargo BINARY args...` and `cargo-BINARY args...`.
46pub fn get_args(subcommand: &str) -> Vec<String> {
47    let mut args: Vec<_> = std::env::args().collect();
48    if args.get(1) == Some(&subcommand.to_string()) {
49        // we face a call `cargo [subcommand]`: we need to get rid of the first argument
50        args = args.into_iter().skip(1).collect();
51    }
52    args
53}
54
55/// Our custom rustc driver will *not* be run in an proper terminal,
56/// thus logs would appear uncolored. When no `RUST_LOG_STYLE` env. var.
57/// is set, [`rust_log_style`] checks wether the `cargo hax` command was
58/// run inside a terminal. If it was inside a terminal,
59/// [`rust_log_style`] returns `"always"`, which is the usual default
60/// behavior. Otherwise we return `"never"`. When [`RUST_LOG_STYLE`] is
61/// set, we just return its value.
62const RUST_LOG_STYLE: &str = "RUST_LOG_STYLE";
63fn rust_log_style() -> String {
64    std::env::var(RUST_LOG_STYLE).unwrap_or_else(|_| {
65        if std::io::stderr().is_terminal() {
66            "always".to_string()
67        } else {
68            "never".to_string()
69        }
70    })
71}
72
73/// We set `cfg(hax)` so that client crates can include dependencies
74/// or cfg-gate pieces of code.
75const RUSTFLAGS: &str = "RUSTFLAGS";
76fn rustflags() -> String {
77    let rustflags = std::env::var(RUSTFLAGS).unwrap_or("".into());
78    [rustflags, "--cfg hax".into()].join(" ")
79}
80
81const ENGINE_BINARY_NAME: &str = "hax-engine";
82const ENGINE_BINARY_NOT_FOUND: &str = "The binary [hax-engine] was not found in your [PATH].";
83
84/// Dynamically looks for binary [ENGINE_BINARY_NAME].  First, we
85/// check whether [HAX_ENGINE_BINARY] is set, and use that if it
86/// is. Then, we try to find [ENGINE_BINARY_NAME] in PATH. If not
87/// found, detect whether nodejs is available, download the JS-compiled
88/// engine and use it.
89#[allow(unused_variables, unreachable_code)]
90fn find_hax_engine(message_format: MessageFormat) -> process::Command {
91    use which::which;
92
93    std::env::var("HAX_ENGINE_BINARY")
94        .ok()
95        .map(process::Command::new)
96        .or_else(|| which(ENGINE_BINARY_NAME).ok().map(process::Command::new))
97        .or_else(|| {
98            which("node").ok().and_then(|_| {
99                if let Ok(true) = inquire::Confirm::new(&format!(
100                    "{} Should I try to download it from GitHub?",
101                    ENGINE_BINARY_NOT_FOUND,
102                ))
103                .with_default(true)
104                .prompt()
105                {
106                    let cmd = process::Command::new("node");
107                    let engine_js_path: String =
108                        panic!("TODO: Downloading from GitHub is not supported yet.");
109                    cmd.arg(engine_js_path);
110                    Some(cmd)
111                } else {
112                    None
113                }
114            })
115        })
116        .unwrap_or_else(|| {
117            fn is_opam_setup_correctly() -> bool {
118                std::env::var("OPAM_SWITCH_PREFIX").is_ok()
119            }
120            HaxMessage::EngineNotFound {
121                is_opam_setup_correctly: is_opam_setup_correctly(),
122            }
123            .report(message_format, None);
124            std::process::exit(2);
125        })
126}
127
128const RUST_ENGINE_BINARY_NAME: &str = "hax-rust-engine";
129const RUST_ENGINE_BINARY_NOT_FOUND: &str =
130    "The binary [hax-rust-engine] was not found in your [PATH].";
131
132#[allow(unused_variables, unreachable_code)]
133fn find_rust_hax_engine(message_format: MessageFormat) -> process::Command {
134    use which::which;
135
136    std::env::var("HAX_RUST_ENGINE_BINARY")
137        .ok()
138        .map(process::Command::new)
139        .or_else(|| {
140            which(RUST_ENGINE_BINARY_NAME)
141                .ok()
142                .map(process::Command::new)
143        })
144        .expect(RUST_ENGINE_BINARY_NOT_FOUND)
145}
146
147use hax_types::diagnostics::message::HaxMessage;
148use hax_types::diagnostics::report::ReportCtx;
149
150#[extension_traits::extension(trait ExtHaxMessage)]
151impl HaxMessage {
152    fn report(self, message_format: MessageFormat, rctx: Option<&mut ReportCtx>) {
153        match message_format {
154            MessageFormat::Json => println!("{}", serde_json::to_string(&self).unwrap()),
155            MessageFormat::Human => self.report_styled(rctx),
156        }
157    }
158    fn report_styled(self, rctx: Option<&mut ReportCtx>) {
159        let renderer = Renderer::styled();
160        match self {
161            Self::Diagnostic {
162                diagnostic,
163                working_dir,
164            } => {
165                let mut _rctx = None;
166                let rctx = rctx.unwrap_or_else(|| _rctx.get_or_insert(ReportCtx::default()));
167                diagnostic.with_message(rctx, &working_dir, Level::Error, |msg| {
168                    eprintln!("{}", renderer.render(msg))
169                });
170            }
171            Self::EngineNotFound {
172                is_opam_setup_correctly,
173            } => {
174                use colored::Colorize;
175                let message = format!("hax: {}\n{}\n\n{} {}\n",
176                      &ENGINE_BINARY_NOT_FOUND,
177                      "Please make sure the engine is installed and is in PATH!",
178                      "Hint: With OPAM, `eval $(opam env)` is necessary for OPAM binaries to be in PATH: make sure to run `eval $(opam env)` before running `cargo hax`.".bright_black(),
179                      format!("(diagnostics: {})", if is_opam_setup_correctly { "opam seems okay ✓" } else {"opam seems not okay ❌"}).bright_black()
180            );
181                let message = Level::Error.title(&message);
182                eprintln!("{}", renderer.render(message))
183            }
184            Self::ProducedFile { mut path, wrote } => {
185                // Make path relative if possible
186                if let Ok(current_dir) = std::env::current_dir() {
187                    if let Ok(relative) = path.strip_prefix(current_dir) {
188                        path = PathBuf::from(".").join(relative).to_path_buf();
189                    }
190                }
191                let title = if wrote {
192                    format!("hax: wrote file {}", path.display())
193                } else {
194                    format!("hax: unchanged file {}", path.display())
195                };
196                eprintln!("{}", renderer.render(Level::Info.title(&title)))
197            }
198            Self::HaxEngineFailure { exit_code } => {
199                let title = format!(
200                    "hax: {} exited with non-zero code {}",
201                    ENGINE_BINARY_NAME, exit_code,
202                );
203                eprintln!("{}", renderer.render(Level::Error.title(&title)));
204            }
205            Self::ProfilingData(data) => {
206                fn format_with_dot(shift: u32, n: u64) -> String {
207                    let factor = 10u64.pow(shift);
208                    format!("{}.{}", n / factor, n % factor)
209                }
210                let title = format!(
211                    "hax[profiling]: {}: {}ms, memory={}, {} item{}{}",
212                    data.context,
213                    format_with_dot(6, data.time_ns),
214                    data.memory,
215                    data.quantity,
216                    if data.quantity > 1 { "s" } else { "" },
217                    if data.errored {
218                        " (note: this failed!)"
219                    } else {
220                        ""
221                    }
222                );
223                eprintln!("{}", renderer.render(Level::Info.title(&title)));
224            }
225            Self::Stats { errors_per_item } => {
226                let success_items = errors_per_item.iter().filter(|(_, n)| *n == 0).count();
227                let total = errors_per_item.len();
228                let title = format!(
229                    "hax: {}/{} items were successfully translated ({}% success rate)",
230                    success_items,
231                    total,
232                    (success_items * 100) / total
233                );
234                eprintln!("{}", renderer.render(Level::Info.title(&title)));
235            }
236            Self::CargoBuildFailure => {
237                let title =
238                    "hax: running `cargo build` was not successful, continuing anyway.".to_string();
239                eprintln!("{}", renderer.render(Level::Warning.title(&title)));
240            }
241            Self::WarnExperimentalBackend { backend } => {
242                let title = format!(
243                    "hax: Experimental backend \"{}\" is work in progress.",
244                    backend
245                );
246                eprintln!("{}", renderer.render(Level::Warning.title(&title)));
247            }
248        }
249    }
250}
251
252/// Runs `hax-engine`
253fn run_engine(
254    haxmeta: HaxMeta<hax_frontend_exporter::ThirBody>,
255    id_table: id_table::Table,
256    working_dir: PathBuf,
257    manifest_dir: PathBuf,
258    backend: &BackendOptions<()>,
259    message_format: MessageFormat,
260) -> bool {
261    let engine_options = EngineOptions {
262        hax_version: haxmeta.hax_version,
263        backend: backend.clone(),
264        input: haxmeta.items,
265        impl_infos: haxmeta.impl_infos,
266    };
267    let mut hax_engine_command = if let Backend::Lean { .. }
268    | Backend::GenerateRustEngineNames { .. } =
269        &engine_options.backend.backend
270    {
271        find_rust_hax_engine(message_format)
272    } else {
273        find_hax_engine(message_format)
274    };
275    let mut engine_subprocess = hax_engine_command
276        .stdin(std::process::Stdio::piped())
277        .stdout(std::process::Stdio::piped())
278        .spawn()
279        .inspect_err(|e| {
280            if let std::io::ErrorKind::NotFound = e.kind() {
281                panic!(
282                    "The binary [{}] was not found in your [PATH].",
283                    ENGINE_BINARY_NAME
284                )
285            }
286        })
287        .unwrap();
288
289    let mut error = false;
290    let mut output = Output {
291        diagnostics: vec![],
292        files: vec![],
293        debug_json: None,
294    };
295    {
296        let mut rctx = hax_types::diagnostics::report::ReportCtx::default();
297        let mut stdin = std::io::BufWriter::new(
298            engine_subprocess
299                .stdin
300                .as_mut()
301                .expect("Could not write on stdin"),
302        );
303
304        macro_rules! send {
305            ($value:expr) => {
306                serde_json::to_writer(&mut stdin, $value).unwrap();
307                stdin.write_all(b"\n").unwrap();
308                stdin.flush().unwrap();
309            };
310        }
311
312        id_table::WithTable::run(id_table, engine_options, |with_table| {
313            send!(with_table);
314        });
315
316        let out_dir = backend.output_dir.clone().unwrap_or({
317            let relative_path: PathBuf = [
318                "proofs",
319                format!("{}", backend.backend).as_str(),
320                "extraction",
321            ]
322            .iter()
323            .collect();
324            manifest_dir.join(&relative_path)
325        });
326
327        let stdout = std::io::BufReader::new(engine_subprocess.stdout.take().unwrap());
328        let mut errors_per_item: HashMap<_, usize> = HashMap::new();
329        for msg in stdout.json_lines() {
330            let msg = msg.expect(
331                "Hax engine sent an invalid json value. \
332            This might be caused by debug messages on stdout, \
333            which is reserved for JSON communication with cargo-hax",
334            );
335            use protocol::*;
336            match msg {
337                FromEngine::Exit => break,
338                FromEngine::Diagnostic(diagnostic) => {
339                    error = true;
340                    if backend.dry_run {
341                        output.diagnostics.push(diagnostic.clone())
342                    }
343                    if let Some(owner_id) = &diagnostic.owner_id {
344                        *errors_per_item.entry(owner_id.clone()).or_default() += 1;
345                    }
346                    HaxMessage::Diagnostic {
347                        diagnostic,
348                        working_dir: working_dir.clone(),
349                    }
350                    .report(message_format, Some(&mut rctx));
351                }
352                FromEngine::File(file) => {
353                    if backend.dry_run {
354                        output.files.push(file)
355                    } else {
356                        let path = out_dir.join(&file.path);
357                        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
358                        let mut wrote = false;
359                        if fs::read_to_string(&path).as_ref().ok() != Some(&file.contents) {
360                            std::fs::write(&path, file.contents).unwrap();
361                            wrote = true;
362                        }
363                        if let Some(mut sourcemap) = file.sourcemap.clone() {
364                            sourcemap.sourcesContent = sourcemap
365                                .sources
366                                .iter()
367                                .map(PathBuf::from)
368                                .map(|path| {
369                                    if path.is_absolute() {
370                                        path
371                                    } else {
372                                        working_dir.join(path).to_path_buf()
373                                    }
374                                })
375                                .map(|path| fs::read_to_string(path).ok())
376                                .collect();
377                            let f = std::fs::File::create(path.with_file_name(format!(
378                                "{}.map",
379                                path.file_name().unwrap().to_string_lossy()
380                            )))
381                            .unwrap();
382                            serde_json::to_writer(std::io::BufWriter::new(f), &sourcemap).unwrap()
383                        }
384                        HaxMessage::ProducedFile { path, wrote }.report(message_format, None)
385                    }
386                }
387                FromEngine::DebugString(debug) => {
388                    output.debug_json = Some(debug);
389                }
390                FromEngine::PrettyPrintDiagnostic(diag) => {
391                    send!(&ToEngine::PrettyPrintedDiagnostic(format!("{}", diag)));
392                }
393                FromEngine::PrettyPrintRust(code) => {
394                    let code = match syn::parse_file(&code) {
395                        Ok(file) => match std::panic::catch_unwind(|| prettyplease::unparse(&file))
396                        {
397                            Ok(pp) => Ok(pp),
398                            Err(err) => Err(format!("prettyplease panicked with: {:#?}", err)),
399                        },
400                        Err(err) => Err(format!("{}", err)),
401                    };
402                    send!(&ToEngine::PrettyPrintedRust(code));
403                }
404                FromEngine::ProfilingData(profiling_data) => {
405                    HaxMessage::ProfilingData(profiling_data).report(message_format, None)
406                }
407                FromEngine::ItemProcessed(items) => {
408                    for item in items {
409                        errors_per_item.insert(item, 0);
410                    }
411                }
412                FromEngine::Ping => {
413                    send!(&ToEngine::Pong);
414                }
415            }
416        }
417        if backend.stats {
418            HaxMessage::Stats {
419                errors_per_item: errors_per_item.into_iter().collect(),
420            }
421            .report(message_format, None)
422        }
423        drop(stdin);
424    }
425
426    let exit_status = engine_subprocess.wait().unwrap();
427    if !exit_status.success() {
428        HaxMessage::HaxEngineFailure {
429            exit_code: exit_status.code().unwrap_or(-1),
430        }
431        .report(message_format, None);
432        std::process::exit(1);
433    }
434
435    if backend.dry_run {
436        serde_json::to_writer(std::io::BufWriter::new(std::io::stdout()), &output).unwrap()
437    }
438    if let Some(debug_json) = &output.debug_json {
439        use DebugEngineMode;
440        match &backend.debug_engine {
441            Some(DebugEngineMode::Interactive) => {
442                eprintln!("----------------------------------------------");
443                eprintln!("----------------------------------------------");
444                eprintln!("----------------------------------------------");
445                eprintln!("-- Engine debug mode. Press CTRL+C to exit. --");
446                eprintln!("----------------------------------------------");
447                eprintln!("----------------------------------------------");
448                eprintln!("----------------------------------------------");
449                engine_debug_webapp::run(|| debug_json.clone())
450            }
451            Some(DebugEngineMode::File(_file)) if !backend.dry_run => {
452                println!("{}", debug_json)
453            }
454            _ => (),
455        }
456    }
457
458    error
459}
460
461/// Uses `cargo metadata` to compute a derived target directory.
462fn target_dir(suffix: &str) -> PathBuf {
463    let metadata = cargo_metadata::MetadataCommand::new().exec().unwrap();
464    let mut dir = metadata.target_directory;
465    dir.push(suffix);
466    dir.into()
467}
468
469/// Gets hax version: if hax is being compiled from a dirty git repo,
470/// then this function taints the hax version with the hash of the
471/// current executable. This makes sure cargo doesn't cache across
472/// different versions of hax, for more information see
473/// https://github.com/hacspec/hax/issues/801.
474fn get_hax_version() -> String {
475    let mut version = hax_types::HAX_VERSION.to_string();
476    if env!("HAX_GIT_IS_DIRTY") == "true" {
477        version += &std::env::current_exe()
478            .ok()
479            .and_then(|exe_path| std::fs::read(exe_path).ok())
480            .map(|contents| {
481                use std::hash::{DefaultHasher, Hash, Hasher};
482                let mut s = DefaultHasher::new();
483                contents.hash(&mut s);
484                format!("hash-exe-{}", s.finish())
485            })
486            .expect("Expect read path")
487    }
488
489    version
490}
491
492/// Returns the path to the custom rustc driver used by cargo-hax.
493///
494/// This function retrieves the path of the current executable (i.e. `cargo-hax`), determines its
495/// parent directory, and then appends the driver executable name `"driver-hax-frontend-exporter"` to it.
496/// This path is used to locate the custom rustc driver that computes `haxmeta` files.
497fn get_hax_rustc_driver_path() -> PathBuf {
498    std::env::current_exe()
499        .expect("Could not get the current executable path for `cargo-hax`.")
500        .parent().expect("The executable `cargo-hax` is supposed to be a file, which is supposed to have a parent folder.")
501        .join("driver-hax-frontend-exporter")
502}
503
504/// Calls `cargo` with a custom driver which computes `haxmeta` files
505/// in `TARGET`. One `haxmeta` file is produced by crate. Each
506/// `haxmeta` file contains the full AST of one crate.
507fn compute_haxmeta_files(options: &Options) -> (Vec<EmitHaxMetaMessage>, i32) {
508    let mut cmd = {
509        let mut cmd = process::Command::new("cargo");
510        if let Some(toolchain) = toolchain() {
511            cmd.env("RUSTUP_TOOLCHAIN", toolchain);
512        }
513        cmd.args(["check".into()].iter().chain(options.cargo_flags.iter()));
514        const COLOR_FLAG: &str = "--color";
515        let explicit_color_flag = options.cargo_flags.iter().any(|flag| flag == COLOR_FLAG);
516        if !explicit_color_flag && std::io::stderr().is_terminal() {
517            cmd.args([COLOR_FLAG, "always"]);
518        }
519        const MSG_FMT_FLAG: &str = "--message-format";
520        let explicit_msg_fmt_flag = options.cargo_flags.iter().any(|flag| flag == MSG_FMT_FLAG);
521        if !explicit_msg_fmt_flag && options.message_format == MessageFormat::Json {
522            cmd.args([MSG_FMT_FLAG, "json"]);
523        }
524        cmd.stderr(std::process::Stdio::piped());
525        if !options.no_custom_target_directory {
526            cmd.env("CARGO_TARGET_DIR", target_dir("hax"));
527        };
528        cmd.env("RUSTC_WORKSPACE_WRAPPER", get_hax_rustc_driver_path())
529            .env(RUST_LOG_STYLE, rust_log_style())
530            .env(RUSTFLAGS, rustflags())
531            .env("HAX_CARGO_CACHE_KEY", get_hax_version())
532            .env(
533                ENV_VAR_OPTIONS_FRONTEND,
534                serde_json::to_string(&options)
535                    .expect("Options could not be converted to a JSON string"),
536            );
537        cmd
538    };
539
540    let mut child = cmd.spawn().unwrap();
541    let haxmeta_files = {
542        let mut haxmeta_files = vec![];
543        let stderr = child.stderr.take().unwrap();
544        let stderr = std::io::BufReader::new(stderr);
545        for line in std::io::BufReader::new(stderr).lines() {
546            if let Ok(line) = line {
547                if let Some(msg) = line.strip_prefix(HAX_DRIVER_STDERR_PREFIX) {
548                    use HaxDriverMessage;
549                    let msg = serde_json::from_str(msg).unwrap();
550                    match msg {
551                        HaxDriverMessage::EmitHaxMeta(data) => haxmeta_files.push(data),
552                    }
553                } else {
554                    eprintln!("{}", line);
555                }
556            }
557        }
558        haxmeta_files
559    };
560
561    let status = child
562        .wait()
563        .expect("`driver-hax-frontend-exporter`: could not start?");
564
565    let exit_code = if !status.success() {
566        HaxMessage::CargoBuildFailure.report(options.message_format, None);
567        status.code().unwrap_or(254)
568    } else {
569        0
570    };
571
572    (haxmeta_files, exit_code)
573}
574
575/// Run the command given by the user
576fn run_command(options: &Options, haxmeta_files: Vec<EmitHaxMetaMessage>) -> bool {
577    match options.command.clone() {
578        Command::JSON {
579            output_file,
580            kind,
581            include_extra,
582            use_ids,
583            ..
584        } => {
585            with_kind_type!(kind, <Body>|| {
586                for EmitHaxMetaMessage { path, .. } in haxmeta_files {
587                    let (haxmeta, id_table): (HaxMeta<Body>, _) = HaxMeta::read(fs::File::open(&path).unwrap());
588                    let dest = output_file.open_or_stdout();
589
590                    (if include_extra {
591                        let data = WithDefIds {
592                            def_ids: haxmeta.def_ids,
593                            impl_infos: haxmeta.impl_infos,
594                            items: haxmeta.items,
595                            comments: haxmeta.comments,
596                        };
597                        if use_ids {
598                            id_table::WithTable::run(id_table, data, |with_table| {
599                                serde_json::to_writer(dest, with_table)
600                            })
601                        } else {
602                            serde_json::to_writer(dest, &data)
603                        }
604                    } else {
605                        if use_ids {
606                            id_table::WithTable::run(id_table, haxmeta.items, |with_table| {
607                                serde_json::to_writer(dest, with_table)
608                            })
609                        } else {
610                            serde_json::to_writer(dest, &haxmeta.items)
611                        }
612                    })
613                        .unwrap()
614
615                }
616            });
617            false
618        }
619        Command::Backend(backend) => {
620            use Backend;
621            use hax_frontend_exporter::ThirBody as Body;
622
623            if matches!(backend.backend, Backend::Easycrypt | Backend::ProVerif(..)) {
624                HaxMessage::WarnExperimentalBackend {
625                    backend: backend.backend.clone(),
626                }
627                .report(options.message_format, None);
628            }
629
630            let mut error = false;
631            for EmitHaxMetaMessage {
632                working_dir,
633                manifest_dir,
634                path,
635            } in haxmeta_files
636            {
637                let (haxmeta, id_table): (HaxMeta<Body>, _) =
638                    HaxMeta::read(fs::File::open(&path).unwrap());
639
640                error = error
641                    || run_engine(
642                        haxmeta,
643                        id_table,
644                        working_dir,
645                        manifest_dir,
646                        &backend,
647                        options.message_format,
648                    );
649            }
650            error
651        }
652    }
653}
654
655fn main() {
656    let args: Vec<String> = get_args("hax");
657    let mut options = match &args[..] {
658        [_, kw] if kw == "__json" => serde_json::from_str(
659            &std::env::var(ENV_VAR_OPTIONS_FRONTEND).unwrap_or_else(|_| {
660                panic!(
661                    "Cannot find environnement variable {}",
662                    ENV_VAR_OPTIONS_FRONTEND
663                )
664            }),
665        )
666        .unwrap_or_else(|_| {
667            panic!(
668                "Invalid value for the environnement variable {}",
669                ENV_VAR_OPTIONS_FRONTEND
670            )
671        }),
672        _ => Options::parse_from(args.iter()),
673    };
674    options.normalize_paths();
675
676    let (haxmeta_files, exit_code) = compute_haxmeta_files(&options);
677    let error = run_command(&options, haxmeta_files);
678
679    std::process::exit(if exit_code == 0 && error {
680        1
681    } else {
682        exit_code
683    })
684}