Skip to main content

rl_lang/tooling/
package.rs

1//! rl package - bundle a .rl program into a self-contained binary.
2//!
3//! Copies the rl binary itself, then appends the source text and a
4//! magic footer so the copy can detect and run the embedded program at
5//! startup without any arguments.
6//!
7//! # Binary layout after packaging
8//!
9//! [ original rl binary bytes ]
10//! [ rl source text (UTF-8)   ]
11//! [ magic marker: \x00RL_PACKAGE_V1\x00 ]
12//! [ source length: u64 little-endian    ]
13//!
14//!
15//! The magic + length are appended *after* the source so detection only
16//! needs to read the last few bytes — no full scan required.
17
18use std::collections::HashSet;
19use std::io::Write;
20use std::path::{Path, PathBuf};
21
22const MAGIC: &[u8] = b"\x00RL_PACKAGE_V1\x00";
23const MAGIC_LEN: usize = 15;
24const FOOTER_LEN: usize = MAGIC_LEN + 8;
25
26/// Searches the running binary's own bytes for an embedded rl program.
27///
28/// Returns Some(source) if this binary was produced by rl package,
29/// or None if it is a plain rl binary.
30pub fn find_embedded() -> Option<String> {
31    let path = std::env::current_exe().ok()?;
32    let bytes = std::fs::read(path).ok()?;
33
34    if bytes.len() < FOOTER_LEN {
35        return None;
36    }
37
38    // read length from the final 8 bytes
39    let len_start = bytes.len() - 8;
40    let len = u64::from_le_bytes(bytes[len_start..].try_into().ok()?) as usize;
41
42    // verify magic sits just before the length
43    let magic_start = len_start.checked_sub(MAGIC_LEN)?;
44    if &bytes[magic_start..len_start] != MAGIC {
45        return None;
46    }
47
48    // source sits just before the magic
49    let source_start = magic_start.checked_sub(len)?;
50    let source_end = magic_start;
51
52    String::from_utf8(bytes[source_start..source_end].to_vec()).ok()
53}
54
55/// Packages source_path into a self-contained binary at output_path.
56///
57/// Copies the running rl binary verbatim, then appends the rl source
58/// and the magic footer. The output is made executable on Unix.
59///
60/// Prints an error and exits with code 1 on any failure.
61pub fn package(source_path: &str, output_path: &str) {
62    if let Err(e) = try_package(source_path, output_path) {
63        eprintln!("error: packaging failed: {}", e);
64        std::process::exit(1);
65    }
66    println!("packaged '{}' -> '{}'", source_path, output_path);
67}
68
69fn try_package(source_path: &str, output_path: &str) -> std::io::Result<()> {
70    let source = bundle(source_path)?;
71    let self_bytes = std::fs::read(std::env::current_exe()?)?;
72
73    let mut out = std::fs::File::create(output_path)?;
74    out.write_all(&self_bytes)?;
75    out.write_all(source.as_bytes())?;
76    out.write_all(MAGIC)?;
77    out.write_all(&(source.len() as u64).to_le_bytes())?;
78
79    #[cfg(unix)]
80    {
81        use std::os::unix::fs::PermissionsExt;
82        let mut perms = std::fs::metadata(output_path)?.permissions();
83        perms.set_mode(0o755);
84        std::fs::set_permissions(output_path, perms)?;
85    }
86
87    Ok(())
88}
89
90fn bundle(entry_path: &str) -> std::io::Result<String> {
91    let mut visited = HashSet::new();
92    bundle_inner(Path::new(entry_path), &mut visited)
93}
94
95fn bundle_inner(path: &Path, visited: &mut HashSet<PathBuf>) -> std::io::Result<String> {
96    let canonical = path.canonicalize()?;
97    if visited.contains(&canonical) {
98        return Ok(String::new());
99    }
100    visited.insert(canonical.clone());
101
102    let source = std::fs::read_to_string(path)?;
103    let base_dir = path.parent().unwrap_or(Path::new("."));
104    let mut output = String::new();
105
106    for line in source.lines() {
107        let trimmed = line.trim();
108
109        if let Some(rest) = trimmed.strip_prefix("get ") {
110            // extract the file name from either form:
111            // "get csv"  ->  file_part = "csv"
112            // "get x, y from csv"  ->  file_part = "csv"
113            let file_part = if let Some(from_idx) = rest.find(" from ") {
114                rest[from_idx + 6..].trim()
115            } else {
116                rest.trim()
117            };
118
119            // only inline local files, not std::
120            if !file_part.contains("::") {
121                let rel: PathBuf = file_part.split('/').collect();
122                let file_path = base_dir.join(rel).with_extension("rl");
123                match bundle_inner(&file_path, visited) {
124                    Ok(inlined) => {
125                        output.push_str(&inlined);
126                        output.push('\n');
127                        continue;
128                    }
129                    Err(e) => {
130                        eprintln!("warning: could not bundle '{}': {}", file_path.display(), e);
131                    }
132                }
133            }
134        }
135
136        output.push_str(line);
137        output.push('\n');
138    }
139
140    Ok(output)
141}