1
//! The implementation of the Zork++ cache, for persisting data in between process
2

            
3
pub mod compile_commands;
4

            
5
use chrono::{DateTime, Utc};
6
use color_eyre::{eyre::Context, Result};
7

            
8
use std::collections::HashMap;
9
use std::fmt::Debug;
10

            
11
use std::{
12
    fs::File,
13
    path::{Path, PathBuf},
14
    time::Instant,
15
};
16

            
17
use crate::config_file::ZorkConfigFile;
18
use crate::domain::commands::command_lines::{Commands, SourceCommandLine};
19
use crate::domain::target::TargetIdentifier;
20
use crate::domain::translation_unit::{TranslationUnit, TranslationUnitKind};
21
use crate::project_model::sourceset::SourceFile;
22
use crate::utils::constants::{dir_names, error_messages};
23
use crate::{
24
    cli::input::CliArgs,
25
    project_model,
26
    project_model::{compiler::CppCompiler, ZorkModel},
27
    utils::{self},
28
};
29
use serde::{Deserialize, Serialize};
30

            
31
use crate::project_model::compiler::StdLibMode;
32
use crate::utils::constants;
33

            
34
/// Standalone utility for load from the file system the Zork++ cache file
35
/// for the target [`CppCompiler`]
36
2
pub fn load<'a>(
37
    config: &ZorkConfigFile<'a>,
38
    cli_args: &CliArgs,
39
    project_root: &Path,
40
) -> Result<ZorkCache<'a>> {
41
2
    let compiler: CppCompiler = config.compiler.cpp_compiler.into();
42
4
    let output_dir = Path::new(project_root).join(
43
2
        config
44
            .build
45
            .as_ref()
46
2
            .and_then(|build_attr| build_attr.output_dir)
47
            .unwrap_or(dir_names::DEFAULT_OUTPUT_DIR),
48
    );
49
2
    let cache_path = output_dir.join(constants::ZORK).join(dir_names::CACHE);
50

            
51
4
    let cache_file_path = cache_path
52
2
        .join(compiler.as_ref())
53
2
        .with_extension(constants::CACHE_FILE_EXT);
54

            
55
4
    let mut cache = if !cache_file_path.exists() {
56
2
        helpers::create_cache(cache_path, cache_file_path, compiler)?
57
    } else if cache_path.exists() && cli_args.clear_cache {
58
        helpers::clear_cache(&cache_file_path)?
59
    } else {
60
        log::trace!(
61
            "Loading Zork++ cache file for {compiler} at: {:?}",
62
            cache_file_path
63
        );
64
        utils::fs::load_and_deserialize(&cache_file_path)
65
            .with_context(|| "Error loading the Zork++ cache")?
66
    };
67

            
68
2
    cache.metadata.process_no += 1;
69

            
70
2
    Ok(cache)
71
2
}
72

            
73
14
#[derive(Serialize, Deserialize, Default)]
74
pub struct ZorkCache<'a> {
75
6
    pub compilers_metadata: CompilersMetadata<'a>,
76
6
    pub generated_commands: Commands<'a>,
77
6
    pub metadata: CacheMetadata,
78
}
79

            
80
impl<'a> ZorkCache<'a> {
81
2
    pub fn save(&mut self, program_data: &ZorkModel<'_>) -> Result<()> {
82
2
        self.run_final_tasks(program_data)?;
83
2
        self.metadata.last_program_execution = Utc::now();
84

            
85
2
        utils::fs::save_file(&self.metadata.cache_file_path, self)
86
            .with_context(|| error_messages::FAILURE_SAVING_CACHE)
87
2
    }
88

            
89
11
    pub fn get_cmd_for_translation_unit_kind<T: TranslationUnit<'a>>(
90
        &mut self,
91
        translation_unit: &T,
92
        translation_unit_kind: &TranslationUnitKind<'a>,
93
    ) -> Option<&mut SourceCommandLine<'a>> {
94
11
        match translation_unit_kind {
95
2
            TranslationUnitKind::ModuleInterface => self.get_module_ifc_cmd(translation_unit),
96
4
            TranslationUnitKind::ModuleImplementation => self.get_module_impl_cmd(translation_unit),
97
4
            TranslationUnitKind::SourceFile(for_target) => {
98
4
                self.get_source_cmd(translation_unit, for_target)
99
            }
100
1
            TranslationUnitKind::SystemHeader => self.get_system_module_cmd(translation_unit),
101
            TranslationUnitKind::ModularStdLib(stdlib_mode) => match stdlib_mode {
102
                StdLibMode::Cpp => self.get_cpp_stdlib_cmd(),
103
                StdLibMode::CCompat => self.get_ccompat_stdlib_cmd(),
104
            },
105
        }
106
11
    }
107

            
108
2
    fn get_module_ifc_cmd<T: TranslationUnit<'a>>(
109
        &mut self,
110
        module_interface: &T,
111
    ) -> Option<&mut SourceCommandLine<'a>> {
112
2
        self.generated_commands
113
            .modules
114
            .interfaces
115
            .iter_mut()
116
            .find(|cached_tu| module_interface.path().eq(&cached_tu.path()))
117
2
    }
118

            
119
4
    fn get_module_impl_cmd<T: TranslationUnit<'a>>(
120
        &mut self,
121
        module_impl: &T,
122
    ) -> Option<&mut SourceCommandLine<'a>> {
123
4
        self.generated_commands
124
            .modules
125
            .implementations
126
            .iter_mut()
127
2
            .find(|cached_tu| module_impl.path().eq(&cached_tu.path()))
128
4
    }
129

            
130
4
    fn get_source_cmd<T: TranslationUnit<'a>>(
131
        &mut self,
132
        source: &T,
133
        for_target: &TargetIdentifier<'a>,
134
    ) -> Option<&mut SourceCommandLine<'a>> {
135
4
        self.generated_commands
136
            .targets
137
            .get_mut(for_target)
138
4
            .and_then(|target| {
139
8
                target
140
                    .sources
141
                    .iter_mut()
142
4
                    .find(|cached_tu| source.path().eq(&cached_tu.path()))
143
4
            })
144
4
    }
145

            
146
    /// Gets the target [`SystemModule`] generated [`SourceCommandLine`] from the cache
147
    ///
148
    /// NOTE: While we don't implement the lookup of the directory of the installed system headers,
149
    /// we are using some tricks to matching the generated command, but is not robust
150
1
    fn get_system_module_cmd<T: TranslationUnit<'a>>(
151
        &mut self,
152
        system_module: &T,
153
    ) -> Option<&mut SourceCommandLine<'a>> {
154
1
        self.generated_commands
155
            .modules
156
            .system_modules
157
            .iter_mut()
158
            .find(|cached_tu| system_module.file_stem().eq(cached_tu.filename()))
159
1
    }
160

            
161
    pub fn get_cpp_stdlib_cmd_by_kind(
162
        &mut self,
163
        stdlib_mode: StdLibMode,
164
    ) -> Option<&mut SourceCommandLine<'a>> {
165
        match stdlib_mode {
166
            StdLibMode::Cpp => self.generated_commands.modules.cpp_stdlib.as_mut(),
167
            StdLibMode::CCompat => self.generated_commands.modules.c_compat_stdlib.as_mut(),
168
        }
169
    }
170

            
171
    pub fn set_cpp_stdlib_cmd_by_kind(
172
        &mut self,
173
        stdlib_mode: StdLibMode,
174
        cmd_line: Option<SourceCommandLine<'a>>,
175
    ) {
176
        match stdlib_mode {
177
            StdLibMode::Cpp => self.generated_commands.modules.cpp_stdlib = cmd_line,
178
            StdLibMode::CCompat => self.generated_commands.modules.c_compat_stdlib = cmd_line,
179
        }
180
    }
181
    fn get_cpp_stdlib_cmd(&mut self) -> Option<&mut SourceCommandLine<'a>> {
182
        self.generated_commands.modules.cpp_stdlib.as_mut()
183
    }
184

            
185
    fn get_ccompat_stdlib_cmd(&mut self) -> Option<&mut SourceCommandLine<'a>> {
186
        self.generated_commands.modules.c_compat_stdlib.as_mut()
187
    }
188

            
189
    /// The tasks associated with the cache after load it from the file system
190
2
    pub fn process_compiler_metadata(&mut self, program_data: &'a ZorkModel<'_>) -> Result<()> {
191
2
        let compiler = program_data.compiler.cpp_compiler;
192

            
193
        if cfg!(target_os = "windows") && compiler.eq(&CppCompiler::MSVC) {
194
            msvc::load_metadata(self, program_data)?
195
2
        } else if compiler.eq(&CppCompiler::CLANG) {
196
2
            clang::load_metadata(self, program_data)?
197
        }
198

            
199
2
        Ok(())
200
2
    }
201

            
202
    /// Runs the tasks just before end the program and save the cache
203
2
    fn run_final_tasks(&mut self, program_data: &ZorkModel<'_>) -> Result<()> {
204
2
        if self.metadata.cfg_modified {
205
2
            project_model::save(program_data, self)?;
206
        }
207

            
208
2
        if program_data.project.compilation_db && self.metadata.generate_compilation_database {
209
2
            let compile_commands_time = Instant::now();
210
4
            compile_commands::map_generated_commands_to_compilation_db(program_data, self)?;
211
2
            log::debug!(
212
                "Zork++ took a total of {:?} ms on generate the compilation database",
213
                compile_commands_time.elapsed().as_millis()
214
            );
215
        }
216

            
217
2
        Ok(())
218
2
    }
219

            
220
    /// Method that returns the HashMap that holds the environmental variables that must be passed
221
    /// to the underlying shell
222
    #[inline(always)]
223
    pub fn get_process_env_args(&'a mut self, compiler: CppCompiler) -> &'a EnvVars {
224
        match compiler {
225
            CppCompiler::MSVC => &self.compilers_metadata.msvc.env_vars,
226
            CppCompiler::CLANG => &self.compilers_metadata.clang.env_vars,
227
            CppCompiler::GCC => &self.compilers_metadata.gcc.env_vars,
228
        }
229
    }
230

            
231
    /// Returns a view of borrowed data over all the generated commands for a target
232
    // TODO: guess that this method is not being used, due to the &self parameter, which provokes
233
    // lots of lifetime issues on the codebase. Remove it.
234
2
    pub fn get_all_commands_iter(&self) -> impl Iterator<Item = &SourceCommandLine> + Debug + '_ {
235
2
        let generated_commands = &self.generated_commands;
236

            
237
10
        generated_commands
238
            .modules
239
            .cpp_stdlib
240
            .as_slice()
241
            .iter()
242
2
            .chain(generated_commands.modules.c_compat_stdlib.as_slice().iter())
243
2
            .chain(generated_commands.modules.interfaces.iter())
244
2
            .chain(generated_commands.modules.implementations.iter())
245
            .chain(
246
2
                generated_commands
247
                    .targets
248
                    .values()
249
4
                    .flat_map(|target| target.sources.iter()),
250
            )
251
2
    }
252

            
253
    /// The current integer value that is the total of commands generated for all the
254
    /// [`TranslationUnit`] declared in the user's configuration file, without counting the linker
255
    /// one for the current target
256
2
    pub fn count_total_generated_commands(&self) -> usize {
257
2
        let latest_commands = &self.generated_commands;
258

            
259
8
        latest_commands.modules.interfaces.len()
260
2
            + latest_commands.modules.implementations.len()
261
2
            + latest_commands.modules.system_modules.len()
262
            + 2 // the cpp_stdlib and the c_compat_stdlib
263
6
            + latest_commands.targets.values().flat_map(|target| target.sources.iter()).count()
264
2
    }
265
}
266

            
267
/// A struct for holding Zork++ internal details about its configuration, procedures or runtime
268
/// statuses
269
18
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
270
pub struct CacheMetadata {
271
8
    pub process_no: u32,
272
8
    pub last_program_execution: DateTime<Utc>,
273
8
    pub cache_file_path: PathBuf,
274
8
    pub project_model_file_path: PathBuf,
275
    // Internal helpers
276
    #[serde(skip)]
277
8
    pub generate_compilation_database: bool,
278
    #[serde(skip)]
279
8
    pub cfg_modified: bool,
280
}
281

            
282
/// Type alias for the underlying key-value based collection of environmental variables
283
pub type EnvVars = HashMap<String, String>;
284

            
285
14
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
286
pub struct CompilersMetadata<'a> {
287
    // TODO: shouldn't this be an Enum or a fat pointer?
288
6
    pub msvc: MsvcMetadata<'a>,
289
6
    pub clang: ClangMetadata,
290
6
    pub gcc: GccMetadata,
291
}
292

            
293
14
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
294
pub struct MsvcMetadata<'a> {
295
6
    pub compiler_version: Option<String>,
296
6
    pub dev_commands_prompt: Option<String>,
297
6
    pub vs_stdlib_path: SourceFile<'a>, // std.ixx path for the MSVC std lib location
298
6
    pub vs_ccompat_stdlib_path: SourceFile<'a>, // std.compat.ixx path for the MSVC std lib location
299
6
    pub stdlib_bmi_path: PathBuf,       // BMI byproduct after build in it at the target out dir of
300
    // the user
301
6
    pub stdlib_obj_path: PathBuf, // Same for the .obj file
302
    // Same as the ones defined for the C++ std lib, but for the C std lib
303
6
    pub ccompat_stdlib_bmi_path: PathBuf,
304
6
    pub ccompat_stdlib_obj_path: PathBuf,
305
    // The environmental variables that will be injected to the underlying invoking shell
306
6
    pub env_vars: EnvVars, // TODO: also, the EnvVars can be held in the CompilersMetadata attr,
307
                           // since every Cache entity is already unique per compilation process?
308
}
309

            
310
16
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
311
pub struct ClangMetadata {
312
7
    pub env_vars: EnvVars,
313
7
    pub version: String,
314
7
    pub major: i32,
315
7
    pub minor: i32,
316
7
    pub patch: i32,
317
7
    pub installed_dir: String,
318
7
    pub libcpp_path: PathBuf,
319
7
    pub stdlib_pcm: PathBuf,
320
7
    pub ccompat_pcm: PathBuf,
321
}
322

            
323
14
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
324
pub struct GccMetadata {
325
6
    pub env_vars: EnvVars,
326
}
327

            
328
/// Helper procedures to process cache data for Microsoft's MSVC
329
mod clang {
330
    use color_eyre::eyre::{self, Context, ContextCompat, Result};
331
    use regex::Regex;
332
    use std::{ffi::OsStr, path::Path, time::Instant};
333
    use walkdir::WalkDir;
334

            
335
    use super::{ClangMetadata, ZorkCache};
336
    use crate::{project_model::ZorkModel, utils::constants::error_messages};
337

            
338
1
    pub(crate) fn load_metadata(
339
        cache: &mut ZorkCache,
340
        program_data: &ZorkModel,
341
    ) -> color_eyre::Result<()> {
342
1
        if cache.compilers_metadata.clang.major != 0 {
343
            log::debug!("Clang metadata already gathered on the cache");
344
            return Ok(());
345
        }
346

            
347
1
        let compiler = program_data.compiler.cpp_compiler;
348
1
        let driver = compiler.get_driver(&program_data.compiler);
349

            
350
        // TODO: if the driver changes on the cfg, how do we know that we have to process this
351
        // cached information again? Just because cache and model are rebuilt again? Can't recall it
352
1
        let clang_cmd_info = std::process::Command::new(OsStr::new(driver.as_ref()))
353
            .arg("-###")
354
            .output()
355
1
            .with_context(|| error_messages::clang::FAILURE_READING_CLANG_DRIVER_INFO)?;
356
        // Combine stdout and stderr into a single output string
357
2
        let combined_output = String::from_utf8_lossy(&clang_cmd_info.stdout).to_string()
358
2
            + &String::from_utf8_lossy(&clang_cmd_info.stderr);
359
        // Typically, the useful information will be in stderr for `clang++ -###` commands
360
1
        cache.compilers_metadata.clang = process_frontend_driver_info(&combined_output)?;
361

            
362
1
        if cache.compilers_metadata.clang.major > 17 {
363
1
            discover_modular_stdlibs(program_data, cache)?;
364
        }
365

            
366
1
        Ok(())
367
1
    }
368

            
369
    fn discover_modular_stdlibs(program_data: &ZorkModel<'_>, cache: &mut ZorkCache) -> Result<()> {
370
        let out_dir = &program_data.build.output_dir;
371

            
372
        let user_declared_libcpp_location = &program_data.compiler.std_lib_installed_dir;
373
        if let Some(user_libcpp_location) = user_declared_libcpp_location {
374
            set_libcpp_installation_dir_by_declared_user_input(user_libcpp_location, cache)?;
375
        } else {
376
            try_find_libcpp_with_assumed_roots(cache)?;
377
        }
378

            
379
        // Byproducts
380
        cache.compilers_metadata.clang.stdlib_pcm = out_dir
381
            .join("clang")
382
            .join("modules")
383
            .join("std")
384
            .join("std")
385
            .with_extension("pcm");
386
        let compat = String::from("compat.");
387
        cache.compilers_metadata.clang.ccompat_pcm = out_dir
388
            .join("clang")
389
            .join("modules")
390
            .join("std") // Folder
391
            .join("std") // Partial filename
392
            .with_extension(
393
                compat.clone()
394
                    + program_data
395
                        .compiler
396
                        .cpp_compiler
397
                        .get_typical_bmi_extension(),
398
            );
399

            
400
        Ok(())
401
    }
402

            
403
    fn set_libcpp_installation_dir_by_declared_user_input(
404
        user_libcpp_declared_location: &Path,
405
        cache: &mut ZorkCache,
406
    ) -> Result<()> {
407
        let user_libcpp_path = Path::new(user_libcpp_declared_location.as_os_str());
408
        if user_libcpp_path.exists() {
409
            log::debug!(
410
                "Found the declared LIBC++ installation at: {:?}",
411
                user_libcpp_path
412
            );
413
            cache.compilers_metadata.clang.libcpp_path = user_libcpp_path.to_path_buf();
414
            Ok(())
415
        } else {
416
            Err(eyre::eyre!(error_messages::clang::WRONG_LIBCPP_DIR))
417
        }
418
    }
419

            
420
    fn try_find_libcpp_with_assumed_roots(cache: &mut ZorkCache) -> Result<()> {
421
        log::info!(
422
            "No libc++ installation path was provided. Trying to find one in the system with the standard modules..\
423
            \nThis may take a while..."
424
        );
425
        let assumed_root = if cfg!(target_os = "windows") {
426
            "C:/" // TODO: should we return an Err and force the user to mandatory
427
                  // provide an installation?
428
        } else {
429
            "/"
430
        };
431

            
432
        let start = Instant::now();
433
        // TODO: maybe we could add some assumed default 'BIG paths' instead of just one
434
        for entry in WalkDir::new(assumed_root)
435
            .into_iter()
436
            .filter_map(Result::ok)
437
        {
438
            let path = entry.path();
439

            
440
            if path.is_dir() && path.file_name().map_or(false, |f| f == "libc++") {
441
                let libcpp_path = path.join("v1");
442
                if libcpp_path.is_dir() {
443
                    let std_cppm_path = libcpp_path.join("std.cppm");
444

            
445
                    if std_cppm_path.exists() {
446
                        log::debug!(
447
                            "Found a valid LIBC++ installation with std.cppm at: {:?}. Took: {:?}",
448
                            libcpp_path,
449
                            start.elapsed()
450
                        );
451
                        cache.compilers_metadata.clang.libcpp_path = libcpp_path;
452
                        return Ok(());
453
                    }
454
                }
455
            }
456
        }
457

            
458
        Err(eyre::eyre!(
459
            error_messages::clang::MISSING_LIBCPP_INSTALLATION
460
        ))
461
    }
462

            
463
1
    fn process_frontend_driver_info(clang_cmd_info: &str) -> Result<ClangMetadata> {
464
1
        let mut clang_metadata = ClangMetadata::default();
465

            
466
5
        for line in clang_cmd_info.lines() {
467
5
            if line.contains("clang version") {
468
1
                let (version_str, major, minor, patch) = extract_clang_version(line)?;
469
1
                clang_metadata.version = version_str;
470
1
                clang_metadata.major = major;
471
1
                clang_metadata.minor = minor;
472
1
                clang_metadata.patch = patch;
473
4
            } else if line.contains("InstalledDir:") {
474
1
                clang_metadata.installed_dir = extract_installed_dir(line)?;
475
            }
476
        }
477

            
478
1
        if clang_metadata.major != 0 {
479
1
            Ok(clang_metadata)
480
        } else {
481
            Err(eyre::eyre!(error_messages::clang::METADATA_GATHER_FAILED))
482
        }
483
1
    }
484

            
485
    /// Helper for extract metainformation about the declared version of the clang's invoked driver
486
4
    fn extract_clang_version(output: &str) -> Result<(String, i32, i32, i32)> {
487
        // Regex pattern to match the version number
488
4
        let version_regex = Regex::new(r"clang version (\d+\.\d+\.\d+)").unwrap();
489

            
490
        // Apply the regex to the output string
491
4
        let captures = version_regex.captures(output);
492
        // Return the captured version number
493
4
        let matched = captures
494
            .with_context(|| error_messages::clang::FAILURE_PARSING_CLANG_VERSION)?
495
            .get(1)
496
4
            .with_context(|| error_messages::clang::FAILURE_PARSING_CLANG_VERSION)?;
497

            
498
4
        let str_v = matched.as_str().to_string();
499
4
        let splitted = str_v.split('.').collect::<Vec<&str>>();
500
4
        let major = splitted
501
            .first()
502
4
            .map(|major| major.parse::<i32>().unwrap())
503
            .with_context(|| error_messages::clang::FAILURE_GETTING_VER_MAJOR)?;
504
4
        let minor = splitted
505
            .get(1)
506
4
            .map(|minor| minor.parse::<i32>().unwrap())
507
            .with_context(|| error_messages::clang::FAILURE_GETTING_VER_MINOR)?;
508
4
        let patch = splitted
509
            .get(2)
510
4
            .map(|patch| patch.parse::<i32>().unwrap())
511
            .with_context(|| error_messages::clang::FAILURE_GETTING_VER_PATCH)?;
512
4
        Ok((str_v, major, minor, patch))
513
4
    }
514

            
515
    /// Helper for extract metainformation about the installed dir of the clang's invoked driver
516
2
    fn extract_installed_dir(line: &str) -> Result<String> {
517
2
        line.split(':')
518
            .collect::<Vec<&str>>()
519
            .get(1)
520
2
            .map(|installed_dir| installed_dir.trim().to_string())
521
            .with_context(|| error_messages::clang::INSTALLED_DIR)
522
2
    }
523

            
524
    #[cfg(test)]
525
    mod tests {
526
        #[test]
527
2
        fn test_clang_version_extractor() {
528
1
            let mock_version: &'static str = "clang version 19.0.0git (git@github.com:llvm/llvm-project.git 60a904b2ad9842b93cc5fa0ad5bda5e22c550b7e)";
529
1
            let expected = ("19.0.0".to_string(), 19, 0, 0);
530
1
            assert_eq!(
531
                expected,
532
1
                super::extract_clang_version(mock_version).unwrap()
533
            );
534

            
535
1
            let mock_version_2: &'static str = "clang version 16.0.5";
536
1
            let expected = ("16.0.5".to_string(), 16, 0, 5);
537
1
            assert_eq!(
538
                expected,
539
1
                super::extract_clang_version(mock_version_2).unwrap()
540
            );
541

            
542
1
            let mock_version_3: &'static str = "Ubuntu clang version 16.0.5 (++20231112100510+7cbf1a259152-1~exp1~20231112100554.106)";
543
1
            let expected = ("16.0.5".to_string(), 16, 0, 5);
544
1
            assert_eq!(
545
                expected,
546
1
                super::extract_clang_version(mock_version_3).unwrap()
547
            )
548
2
        }
549

            
550
        #[test]
551
2
        fn test_clang_installed_dir_extractor() {
552
1
            let mock_installed_dir: &'static str = "InstalledDir: /usr/bin";
553
1
            let expected = "/usr/bin";
554
1
            assert_eq!(
555
                expected,
556
1
                super::extract_installed_dir(mock_installed_dir).unwrap()
557
            );
558
2
        }
559
    }
560
}
561

            
562
/// Helper procedures to process cache data for Microsoft's MSVC
563
mod msvc {
564
    use crate::cache::ZorkCache;
565
    use crate::project_model::sourceset::SourceFile;
566
    use crate::project_model::ZorkModel;
567
    use crate::utils;
568
    use crate::utils::constants::{self, dir_names};
569
    use crate::utils::constants::{env_vars, error_messages};
570
    use color_eyre::eyre::{eyre, Context, ContextCompat, OptionExt};
571
    use regex::Regex;
572
    use std::borrow::Cow;
573
    use std::collections::HashMap;
574
    use std::path::Path;
575

            
576
    /// If *Windows* is the current OS, and the compiler is *MSVC*, then we will try
577
    /// to locate the path of the `vcvars64.bat` script that will set a set of environmental
578
    /// variables that are required to work effortlessly with the Microsoft's compiler.
579
    ///
580
    /// After such effort, we will dump those env vars to a custom temporary file where every
581
    /// env var is registered there in a key-value format, so we can load it into the cache and
582
    /// run this process once per new cache created (cache action 1)
583
    pub(crate) fn load_metadata(
584
        cache: &mut ZorkCache,
585
        program_data: &ZorkModel,
586
    ) -> color_eyre::Result<()> {
587
        let compiler = program_data.compiler.cpp_compiler;
588
        let output_dir = &program_data.build.output_dir;
589

            
590
        let msvc = &mut cache.compilers_metadata.msvc;
591

            
592
        if msvc.dev_commands_prompt.is_none() {
593
            msvc.dev_commands_prompt = utils::fs::find_file(
594
                Path::new(constants::MSVC_REGULAR_BASE_PATH),
595
                constants::MS_ENV_VARS_BAT,
596
            )
597
            .map(|walkdir_entry| {
598
                walkdir_entry.path().to_string_lossy().replace(
599
                    constants::MSVC_REGULAR_BASE_PATH,
600
                    constants::MSVC_REGULAR_BASE_SCAPED_PATH,
601
                )
602
            });
603
            let output = std::process::Command::new(constants::WIN_CMD)
604
                .arg("/c")
605
                .arg(msvc.dev_commands_prompt.as_ref().ok_or_eyre(
606
                    error_messages::msvc::MISSING_OR_CORRUPTED_MSVC_DEV_COMMAND_PROMPT,
607
                )?)
608
                .arg("&&")
609
                .arg("set")
610
                .output()
611
                .with_context(|| error_messages::msvc::FAILURE_LOADING_VS_ENV_VARS)?;
612

            
613
            msvc.env_vars = load_env_vars_from_cmd_output(&output.stdout)?;
614
            // Cloning the useful ones for quick access at call site
615
            msvc.compiler_version = msvc.env_vars.get(env_vars::VS_VERSION).cloned();
616

            
617
            // Check the existence of the VCtools
618
            let vctools_dir = msvc
619
                .env_vars
620
                .get(env_vars::VC_TOOLS_INSTALL_DIR)
621
                .with_context(|| error_messages::msvc::MISSING_VCTOOLS_DIR)?;
622

            
623
            let vs_stdlib_path = Path::new(vctools_dir).join(dir_names::MODULES);
624
            if !vs_stdlib_path.exists() {
625
                return Err(eyre!(error_messages::msvc::STDLIB_MODULES_NOT_FOUND));
626
            }
627

            
628
            msvc.vs_stdlib_path = SourceFile {
629
                path: vs_stdlib_path.clone(),
630
                file_stem: Cow::Borrowed("std"),
631
                extension: compiler.default_module_extension(),
632
            };
633
            msvc.vs_ccompat_stdlib_path = SourceFile {
634
                path: vs_stdlib_path,
635
                file_stem: Cow::Borrowed("std.compat"),
636
                extension: compiler.default_module_extension(),
637
            };
638
            let modular_stdlib_byproducts_path = Path::new(&output_dir)
639
                .join(compiler.as_ref())
640
                .join(dir_names::MODULES)
641
                .join(dir_names::STD) // folder
642
                .join("std"); // filename
643

            
644
            // Saving the paths to the precompiled bmi and obj files of the MSVC std implementation
645
            // that will be used to reference the build of the std as a module
646
            msvc.stdlib_bmi_path =
647
                modular_stdlib_byproducts_path.with_extension(compiler.get_typical_bmi_extension());
648
            msvc.stdlib_obj_path =
649
                modular_stdlib_byproducts_path.with_extension(compiler.get_obj_file_extension());
650

            
651
            let c_modular_stdlib_byproducts_path = modular_stdlib_byproducts_path;
652
            let compat = String::from("compat.");
653
            msvc.ccompat_stdlib_bmi_path = c_modular_stdlib_byproducts_path
654
                .with_extension(compat.clone() + compiler.get_typical_bmi_extension());
655
            msvc.ccompat_stdlib_obj_path = c_modular_stdlib_byproducts_path
656
                .with_extension(compat + compiler.get_obj_file_extension());
657
        }
658

            
659
        Ok(())
660
    }
661

            
662
    /// Convenient helper to manipulate and store the environmental variables as result of invoking
663
    /// the Windows `SET` cmd command
664
    fn load_env_vars_from_cmd_output(stdout: &[u8]) -> color_eyre::Result<HashMap<String, String>> {
665
        let env_vars_str = std::str::from_utf8(stdout)?;
666
        let filter = Regex::new(r"^[a-zA-Z_]+$").unwrap();
667

            
668
        let mut env_vars: HashMap<String, String> = HashMap::new();
669
        for line in env_vars_str.lines() {
670
            // Parse the key-value pair from each line
671
            let mut parts = line.splitn(2, '=');
672
            let key = parts
673
                .next()
674
                .expect(error_messages::msvc::ILL_FORMED_KEY_ON_ENV_VARS_PARSING)
675
                .trim();
676

            
677
            if filter.is_match(key) {
678
                let value = parts.next().unwrap_or_default().trim().to_string();
679
                env_vars.insert(key.to_string(), value);
680
            }
681
        }
682

            
683
        Ok(env_vars)
684
    }
685
}
686

            
687
pub(crate) mod helpers {
688
    use color_eyre::eyre::ContextCompat;
689

            
690
    use self::utils::constants::error_messages;
691
    use super::*;
692
    use crate::domain::translation_unit::TranslationUnitStatus;
693
    use std::path::PathBuf;
694

            
695
2
    pub(crate) fn create_cache<'a>(
696
        cache_path: PathBuf,
697
        cache_file_path: PathBuf,
698
        compiler: CppCompiler,
699
    ) -> Result<ZorkCache<'a>> {
700
2
        File::create(&cache_file_path).with_context(|| error_messages::FAILURE_LOADING_CACHE)?;
701
2
        helpers::initialize_cache(cache_path, cache_file_path, compiler)
702
2
    }
703

            
704
2
    fn initialize_cache<'a>(
705
        cache_path: PathBuf,
706
        cache_file_path: PathBuf,
707
        compiler: CppCompiler,
708
    ) -> Result<ZorkCache<'a>> {
709
4
        let project_model_file_path = cache_path
710
2
            .join(format!("{}_pm", compiler.as_ref()))
711
2
            .with_extension(constants::CACHE_FILE_EXT);
712

            
713
2
        let cache = ZorkCache {
714
2
            metadata: CacheMetadata {
715
                cache_file_path,
716
                project_model_file_path,
717
2
                ..Default::default()
718
            },
719
2
            ..Default::default()
720
2
        };
721

            
722
2
        Ok(cache)
723
2
    }
724

            
725
    pub(crate) fn clear_cache<'a>(cache_file_path: &Path) -> Result<ZorkCache<'a>> {
726
        let mut zork_cache: ZorkCache<'_> = utils::fs::load_and_deserialize(&cache_file_path)
727
            .with_context(|| error_messages::FAILURE_LOADING_CACHE)?;
728

            
729
        // clean the user compilation products
730
        zork_cache.generated_commands.clean_user_modules()?;
731
        zork_cache.generated_commands.clean_targets()?;
732

            
733
        // restore to default some of the metadata properties
734
        zork_cache.metadata.process_no = u32::default();
735
        zork_cache.metadata.last_program_execution = DateTime::default();
736

            
737
        Ok(zork_cache)
738
    }
739

            
740
    /// Checks for those translation units that the process detected that must be deleted from the
741
    /// cache -> [`TranslationUnitStatus::ToDelete`] or if the file has been removed from the
742
    /// Zork++ configuration file or if it has been removed from the fs
743
    ///
744
    /// Can we only call this when we know that the user modified the ZorkCache file for the current iteration?
745
2
    pub(crate) fn check_user_files_removals(
746
        cache: &mut ZorkCache,
747
        program_data: &ZorkModel<'_>,
748
    ) -> Result<bool> {
749
4
        let deletions_on_cfg = remove_if_needed_from_cache_and_count_changes(
750
2
            &mut cache.generated_commands.modules.interfaces,
751
2
            &program_data.modules.interfaces,
752
2
        ) || remove_if_needed_from_cache_and_count_changes(
753
2
            &mut cache.generated_commands.modules.implementations,
754
2
            &program_data.modules.implementations,
755
        ) || {
756
2
            for (target_name, target_data) in cache.generated_commands.targets.iter_mut() {
757
                let changes = remove_if_needed_from_cache_and_count_changes(
758
                    &mut target_data.sources,
759
                    program_data
760
                        .targets
761
                        .get(target_name)
762
                        .with_context(|| error_messages::TARGET_ENTRY_NOT_FOUND)?
763
                        .sources
764
                        .as_slice(),
765
                );
766
                if changes {
767
                    return Ok(true);
768
                }
769
            }
770
2
            return Ok(false);
771
        } || remove_if_needed_from_cache_and_count_changes(
772
            &mut cache.generated_commands.modules.system_modules,
773
            &program_data.modules.sys_modules,
774
        );
775

            
776
        Ok(deletions_on_cfg)
777
2
    }
778

            
779
4
    fn remove_if_needed_from_cache_and_count_changes<'a, T: TranslationUnit<'a>>(
780
        cached_commands: &mut Vec<SourceCommandLine>,
781
        user_declared_translation_units: &[T],
782
    ) -> bool {
783
4
        let removal_conditions = |scl: &SourceCommandLine| -> bool {
784
            scl.status.eq(&TranslationUnitStatus::ToDelete) || {
785
                let r = user_declared_translation_units
786
                    .iter()
787
                    .any(|cc| cc.path().eq(&scl.path()));
788

            
789
                if !r {
790
                    log::debug!("Found translation_unit removed from cfg: {:?}", scl);
791
                }
792
                r
793
            }
794
        };
795

            
796
4
        let total_cached_source_command_lines = cached_commands.len();
797
4
        cached_commands.retain(removal_conditions);
798

            
799
4
        total_cached_source_command_lines > cached_commands.len()
800
4
    }
801
}