1
use std::fs::File;
2
use std::fs::{self};
3
use std::io::Write;
4
use std::path::Path;
5
use std::path::PathBuf;
6

            
7
use duct::Expression;
8
use duct::cmd;
9
use indoc::indoc;
10
use libloading::Library;
11
use log::info;
12
use toml::Table;
13
use toml::Value;
14
use toml::map::Map;
15

            
16
use merc_utilities::MercError;
17

            
18
/// Apply the value from compilation_toml for every given variable as an environment variable.
19
1
fn apply_env(
20
1
    builder: Expression,
21
1
    compilation_toml: &Map<String, Value>,
22
1
    variables: &[&'_ str],
23
1
) -> Result<Expression, MercError> {
24
1
    let mut result = builder;
25
1
    let env = compilation_toml.get("env").ok_or("Missing [env] table")?;
26

            
27
3
    for var in variables {
28
3
        let value = env.get(*var).ok_or("Missing var")?.as_str().ok_or("Not a string")?;
29

            
30
3
        info!("Setting environment variable {var} = {value}");
31
3
        result = result.env(var, value);
32
    }
33

            
34
1
    Ok(result)
35
1
}
36

            
37
/// A library that can be used to generate a crate on-the-fly and load it back
38
/// in after compiling at runtime.
39
pub(crate) struct RuntimeLibrary {
40
    source_dir: PathBuf,
41
    temp_dir: PathBuf,
42
}
43

            
44
impl RuntimeLibrary {
45
    /// Creates a new library that can be compiled at runtime.
46
    ///
47
    /// `dependencies` are extra lines appended verbatim to the generated
48
    /// `[dependencies]` table of the on-the-fly crate.
49
1
    pub(crate) fn new(temp_dir: &Path, dependencies: Vec<String>) -> Result<RuntimeLibrary, MercError> {
50
1
        info!("Creating library in directory {}", temp_dir.to_string_lossy());
51
1
        let source_dir = PathBuf::from(temp_dir).join("src");
52

            
53
        // Create the directory structure for a Cargo project. `create_dir_all`
54
        // tolerates missing parents and an already-existing directory.
55
1
        fs::create_dir_all(&source_dir)?;
56

            
57
        // Write the cargo configuration
58
        {
59
1
            let mut file = File::create(PathBuf::from(temp_dir).join("Cargo.toml"))?;
60
            // `rust-version` is templated from the host crate (workspace-inherited)
61
            // so the generated crate never drifts from the toolchain in use.
62
1
            writeln!(
63
1
                &mut file,
64
                indoc! {"
65
                [package]
66
                name = \"sabre-generated\"
67
                edition = \"2024\"
68
                rust-version = \"{}\"
69
                version = \"1.0.0\"
70
                [workspace]
71

            
72
                [dependencies]"},
73
                env!("CARGO_PKG_RUST_VERSION")
74
            )?;
75

            
76
1
            for dependency in &dependencies {
77
1
                writeln!(&mut file, "{dependency}")?;
78
            }
79

            
80
1
            writeln!(
81
1
                &mut file,
82
                indoc! {"
83
                
84
                [lib]
85
                crate-type = [\"cdylib\", \"rlib\"]            
86
            "}
87
            )?;
88
        }
89

            
90
        // Ignore the created package.
91
        {
92
1
            let mut file = File::create(PathBuf::from(temp_dir).join(".gitignore"))?;
93
1
            writeln!(&mut file, "*")?;
94
        }
95

            
96
1
        Ok(RuntimeLibrary {
97
1
            temp_dir: PathBuf::from(temp_dir),
98
1
            source_dir,
99
1
        })
100
1
    }
101

            
102
    /// Returns the directory in which the source files can be placed.
103
1
    pub(crate) fn source_dir(&self) -> &PathBuf {
104
1
        &self.source_dir
105
1
    }
106

            
107
    /// Compiles the generated crate into a dynamic library and loads it back in.
108
1
    pub(crate) fn compile(&mut self) -> Result<Library, MercError> {
109
1
        let compilation_toml = include_str!(concat!(env!("OUT_DIR"), "/Compilation.toml")).parse::<Table>()?;
110

            
111
        // Compile the dynamic object.
112
1
        info!("Compiling...");
113
1
        let release = !cfg!(debug_assertions);
114
1
        let build_args: &[&str] = if release {
115
            &["build", "--lib", "--release"]
116
        } else {
117
1
            &["build", "--lib"]
118
        };
119
1
        let mut expr = cmd("cargo", build_args).dir(self.temp_dir.as_path());
120
1
        expr = apply_env(expr, &compilation_toml, &["RUSTFLAGS", "CFLAGS", "CXXFLAGS"])?;
121
1
        expr.run()?;
122

            
123
1
        info!("finished.");
124

            
125
        // Figure out the path to the library (it is based on platform: linux, windows and then macos)
126
1
        let profile = if release { "release" } else { "debug" };
127
1
        let mut path = self
128
1
            .temp_dir
129
1
            .clone()
130
1
            .join(format!("./target/{profile}/libsabre_generated.so"));
131
1
        if !path.exists() {
132
            path = self
133
                .temp_dir
134
                .clone()
135
                .join(format!("./target/{profile}/sabre_generated.dll"));
136
            if !path.exists() {
137
                path = self
138
                    .temp_dir
139
                    .clone()
140
                    .join(format!("./target/{profile}/libsabre_generated.dylib"));
141
                if !path.exists() {
142
                    return Err("Could not find the compiled library!".into());
143
                }
144
            }
145
1
        }
146

            
147
        // Load it back in and call the rewriter.
148
1
        unsafe { Ok(Library::new(&path)?) }
149
1
    }
150
}