Phase 6: Native Executables implementieren und Change archivieren
This commit is contained in:
@@ -11,6 +11,8 @@ name = "tbc"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tb-runner.workspace = true
|
||||
tb-export.workspace = true
|
||||
tb-frontend.workspace = true
|
||||
tb-vm.workspace = true
|
||||
tb-runtime.workspace = true
|
||||
@@ -21,3 +23,9 @@ anyhow.workspace = true
|
||||
tb-ide = { path = "../tb-ide" }
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
signal-hook.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.59", features = ["Win32_System_Console"] }
|
||||
|
||||
70
crates/tb-cli/src/export_cancel.rs
Normal file
70
crates/tb-cli/src/export_cancel.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
//! Prozessabbruch während eines CLI-Exports kontrolliert bis zur Bereinigung führen.
|
||||
#[cfg(unix)]
|
||||
pub struct ExportCancel {
|
||||
flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
handlers: Vec<signal_hook::SigId>,
|
||||
}
|
||||
#[cfg(unix)]
|
||||
impl ExportCancel {
|
||||
pub fn new() -> std::io::Result<Self> {
|
||||
let mut result = Self {
|
||||
flag: Default::default(),
|
||||
handlers: Vec::new(),
|
||||
};
|
||||
for signal in [signal_hook::consts::SIGINT, signal_hook::consts::SIGTERM] {
|
||||
result
|
||||
.handlers
|
||||
.push(signal_hook::flag::register(signal, result.flag.clone())?);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
pub fn cancelled(&self) -> bool {
|
||||
self.flag.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
#[cfg(unix)]
|
||||
impl Drop for ExportCancel {
|
||||
fn drop(&mut self) {
|
||||
for handler in &self.handlers {
|
||||
signal_hook::low_level::unregister(*handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
static CANCELLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
#[cfg(windows)]
|
||||
unsafe extern "system" fn handle(signal: u32) -> i32 {
|
||||
if signal <= 1 {
|
||||
CANCELLED.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
pub struct ExportCancel;
|
||||
#[cfg(windows)]
|
||||
impl ExportCancel {
|
||||
pub fn new() -> std::io::Result<Self> {
|
||||
CANCELLED.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
// Der CLI-Prozess hat genau einen Export. Win32 ruft den statischen Handler auf.
|
||||
if unsafe { windows_sys::Win32::System::Console::SetConsoleCtrlHandler(Some(handle), 1) }
|
||||
== 0
|
||||
{
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
Ok(Self)
|
||||
}
|
||||
pub fn cancelled(&self) -> bool {
|
||||
CANCELLED.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
impl Drop for ExportCancel {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
windows_sys::Win32::System::Console::SetConsoleCtrlHandler(Some(handle), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,10 @@ use tb_ide::{
|
||||
commands::Command,
|
||||
export::{ExportStatus, ProjectStamp},
|
||||
};
|
||||
use tb_runtime::host::{Ereignis, Host};
|
||||
use tb_runtime::{snapshot, value::Value};
|
||||
use tb_vm::interp::RunEvent;
|
||||
use tb_vm::project_io::new_execution;
|
||||
struct Temp(PathBuf);
|
||||
impl Temp {
|
||||
fn new() -> Self {
|
||||
|
||||
@@ -12,10 +12,9 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use tb_runtime::host::{Ereignis, Host};
|
||||
use tb_ui::host::TerminalHost;
|
||||
use tb_vm::interp::{RunEvent, Vm};
|
||||
use tb_vm::project_io::{module_name, new_execution, run_target, SourceLoader};
|
||||
use tb_vm::project_io::{module_name, SourceLoader};
|
||||
|
||||
mod export_cancel;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
@@ -126,215 +125,147 @@ fn cmd_check(args: &[String]) -> ExitCode {
|
||||
}
|
||||
|
||||
fn cmd_build(args: &[String]) -> ExitCode {
|
||||
let (path, module) = match compile(args.first()) {
|
||||
Ok(x) => x,
|
||||
Err(code) => return code,
|
||||
};
|
||||
let out = path.with_extension("tbc");
|
||||
match std::fs::write(&out, module.to_tbc()) {
|
||||
Ok(()) => {
|
||||
println!("{}", out.display());
|
||||
match build(args) {
|
||||
Ok(path) => {
|
||||
println!("{}", path.display());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}: {e}", out.display());
|
||||
Err(error) => {
|
||||
eprintln!("{error:#}");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_run(args: &[String]) -> ExitCode {
|
||||
// Ohne Terminal (Pipe, Skript, CI) läuft das Programm im PipeHost:
|
||||
// Eingabe zeilenweise von stdin, Ausgabe am Ende als Snapshot.
|
||||
let result = match TerminalHost::new() {
|
||||
Ok(mut host) => {
|
||||
let size = host.groesse().ok();
|
||||
let result = run_chain(args, &mut host, size);
|
||||
drop(host); // Alternativschirm verlassen, bevor gedruckt wird
|
||||
result
|
||||
}
|
||||
Err(_) => run_chain(args, &mut PipeHost::new(), None),
|
||||
};
|
||||
let (ereignis, vm) = match result {
|
||||
Ok(result) => result,
|
||||
Err(code) => return code,
|
||||
};
|
||||
print!("{}", tb_runtime::snapshot::text(&vm.rt.screen));
|
||||
// `LPRINT` sammelt im Druckerpuffer; am Programmende geht er in die
|
||||
// Datei LPT1.TXT im aktuellen Verzeichnis (dokumentierte Abweichung —
|
||||
// einen Druckerkanal gibt es plattformübergreifend nicht).
|
||||
if !vm.rt.print.drucker.is_empty() {
|
||||
if let Err(e) = std::fs::write("LPT1.TXT", &vm.rt.print.drucker) {
|
||||
eprintln!("Druckerausgabe nicht schreibbar: {e}");
|
||||
fn build(args: &[String]) -> anyhow::Result<PathBuf> {
|
||||
use anyhow::{bail, ensure, Context};
|
||||
let mut source = None;
|
||||
let mut exe = false;
|
||||
let mut force = false;
|
||||
let mut output = None;
|
||||
let mut target = None;
|
||||
let mut template = None;
|
||||
let mut args = args.iter();
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--exe" => {
|
||||
ensure!(!exe, "Doppeltes --exe");
|
||||
exe = true;
|
||||
}
|
||||
"--force" => {
|
||||
ensure!(!force, "Doppeltes --force");
|
||||
force = true;
|
||||
}
|
||||
"-o" | "--output" | "--target" | "--template" => {
|
||||
let value = args
|
||||
.next()
|
||||
.filter(|v| !v.starts_with('-'))
|
||||
.with_context(|| format!("Wert fehlt für {arg}"))?;
|
||||
let slot = match arg.as_str() {
|
||||
"--target" => &mut target,
|
||||
"--template" => &mut template,
|
||||
_ => &mut output,
|
||||
};
|
||||
ensure!(
|
||||
slot.replace(value.clone()).is_none(),
|
||||
"Doppelte Option {arg}"
|
||||
);
|
||||
}
|
||||
"--" => {
|
||||
for value in args.by_ref() {
|
||||
ensure!(
|
||||
source.replace(value.clone()).is_none(),
|
||||
"Mehrere Quelldateien angegeben"
|
||||
);
|
||||
}
|
||||
}
|
||||
value if value.starts_with('-') => bail!("Unbekannte Build-Option: {value}"),
|
||||
_ => {
|
||||
ensure!(
|
||||
source.replace(arg.clone()).is_none(),
|
||||
"Mehrere Quelldateien angegeben"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match ereignis {
|
||||
RunEvent::Ended => ExitCode::SUCCESS,
|
||||
RunEvent::Stopped { line } => {
|
||||
// STOP außerhalb der IDE: Meldung + Exit-Code ≠ 0 (D6).
|
||||
eprintln!("{}:{line}: STOP in line {line}", vm.current_file());
|
||||
ExitCode::from(3)
|
||||
}
|
||||
RunEvent::Error {
|
||||
code,
|
||||
line,
|
||||
message,
|
||||
} => {
|
||||
eprintln!(
|
||||
"{}:{line}:{}: Runtime error {code}: {message} in line {line}",
|
||||
vm.current_file(),
|
||||
vm.current_source_pos().column
|
||||
);
|
||||
ExitCode::from(2)
|
||||
}
|
||||
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
||||
RunEvent::Interrupted { line } => {
|
||||
eprintln!("Abgebrochen in Zeile {line}");
|
||||
ExitCode::from(3)
|
||||
}
|
||||
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
||||
RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } => ExitCode::from(2),
|
||||
RunEvent::Restart { .. } => unreachable!("RUN wird vom Runner aufgelöst"),
|
||||
let source = source.context(
|
||||
"Aufruf: tbc build <Quelle> [--exe --target <Triple> --template <tbrt> -o <Ziel> --force]",
|
||||
)?;
|
||||
ensure!(
|
||||
exe || (target.is_none() && template.is_none() && output.is_none() && !force),
|
||||
"Native Ausgabeoptionen benötigen --exe"
|
||||
);
|
||||
let selected = if exe {
|
||||
Some(match target {
|
||||
Some(s) => tb_export::Target::parse(&s)?,
|
||||
None => tb_export::Target::host()?,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (path, module) =
|
||||
compile(Some(&source)).map_err(|_| anyhow::anyhow!("Build fehlgeschlagen"))?;
|
||||
if let Some(target) = selected {
|
||||
let out = output.map(PathBuf::from).unwrap_or_else(|| {
|
||||
path.with_extension(if target == tb_export::Target::WindowsAmd64 {
|
||||
"exe"
|
||||
} else {
|
||||
""
|
||||
})
|
||||
});
|
||||
let template = match template {
|
||||
Some(p) => PathBuf::from(p),
|
||||
None => std::env::current_exe()?
|
||||
.parent()
|
||||
.context("Compilerpfad ohne Verzeichnis")?
|
||||
.join("runtimes")
|
||||
.join(target.triple())
|
||||
.join(target.runtime_name()),
|
||||
};
|
||||
let cancel = export_cancel::ExportCancel::new()?;
|
||||
tb_export::export(
|
||||
tb_export::Export {
|
||||
module: &module,
|
||||
target,
|
||||
template: &template,
|
||||
output: &out,
|
||||
overwrite: force,
|
||||
protected: &[path],
|
||||
},
|
||||
&|| cancel.cancelled(),
|
||||
)?;
|
||||
Ok(out)
|
||||
} else {
|
||||
let out = path.with_extension("tbc");
|
||||
std::fs::write(&out, module.to_tbc())?;
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_chain(
|
||||
args: &[String],
|
||||
host: &mut dyn Host,
|
||||
size: Option<(usize, usize)>,
|
||||
) -> Result<(RunEvent, Vm), ExitCode> {
|
||||
fn cmd_run(args: &[String]) -> ExitCode {
|
||||
let Some(first) = args.first() else {
|
||||
eprintln!("Aufruf: tbc run <datei.bas|datei.frm|projekt.mak|datei.tbc>");
|
||||
return Err(ExitCode::from(1));
|
||||
return ExitCode::from(1);
|
||||
};
|
||||
let mut current = PathBuf::from(first);
|
||||
let mut start_line = None;
|
||||
let command = args[1..].join(" ");
|
||||
loop {
|
||||
let current_arg = current.display().to_string();
|
||||
let (path, module) = compile(Some(¤t_arg))?;
|
||||
let mut vm = new_execution(module, &command, size, start_line.take()).map_err(|error| {
|
||||
eprintln!("Runtime error {}: {}", error.0, error);
|
||||
ExitCode::from(2)
|
||||
})?;
|
||||
if !vm.rt.zeitpunkt().1 {
|
||||
eprintln!("Zeitzone nicht ermittelbar — Zeitfunktionen rechnen in UTC.");
|
||||
}
|
||||
let event = vm.run(host);
|
||||
let event =
|
||||
if event == RunEvent::Ended && vm.forms.has_visible_forms() && !vm.is_terminated() {
|
||||
vm.run_visible_forms(host)
|
||||
} else {
|
||||
event
|
||||
};
|
||||
match event {
|
||||
RunEvent::Restart { program, line } => {
|
||||
if let Some(program) = program {
|
||||
current = run_target(&path, &program).map_err(|error| {
|
||||
eprintln!("{error}");
|
||||
ExitCode::from(1)
|
||||
})?;
|
||||
}
|
||||
start_line = line;
|
||||
}
|
||||
event => return Ok((event, vm)),
|
||||
}
|
||||
}
|
||||
tb_runner::run(Path::new(first), &args[1..].join(" "), |path| {
|
||||
compile(Some(&path.display().to_string())).map(|(_, module)| module)
|
||||
})
|
||||
}
|
||||
|
||||
/// Host ohne Terminal: für Pipes und Skripte (`tbc run x.bas < eingabe.txt`).
|
||||
/// Zeigt während des Laufs nichts an; die Ausgabe entsteht am Ende aus dem
|
||||
/// Bildschirm-Snapshot. Tastendrücke kommen zeilenweise von stdin.
|
||||
struct PipeHost {
|
||||
puffer: std::collections::VecDeque<Ereignis>,
|
||||
eof: bool,
|
||||
start: std::time::Instant,
|
||||
}
|
||||
|
||||
impl PipeHost {
|
||||
fn new() -> Self {
|
||||
let mut host = PipeHost {
|
||||
puffer: std::collections::VecDeque::new(),
|
||||
eof: false,
|
||||
start: std::time::Instant::now(),
|
||||
};
|
||||
use std::io::{IsTerminal, Read};
|
||||
let mut stdin = std::io::stdin();
|
||||
if !stdin.is_terminal() {
|
||||
let mut input = String::new();
|
||||
let _ = stdin.read_to_string(&mut input);
|
||||
for line in input.split_inclusive('\n') {
|
||||
for character in line.trim_end_matches(['\r', '\n']).chars() {
|
||||
host.puffer
|
||||
.push_back(Ereignis::Taste(character.to_string(), 0));
|
||||
}
|
||||
host.puffer.push_back(Ereignis::Taste(
|
||||
tb_runtime::host::taste::ENTER.to_string(),
|
||||
0,
|
||||
));
|
||||
}
|
||||
host.puffer.push_back(Ereignis::Ende);
|
||||
host.eof = true;
|
||||
}
|
||||
host
|
||||
}
|
||||
|
||||
/// Eine Zeile von stdin in Tastendrücke zerlegen.
|
||||
fn nachfuellen(&mut self) {
|
||||
use std::io::BufRead;
|
||||
if self.eof {
|
||||
return;
|
||||
}
|
||||
let mut zeile = String::new();
|
||||
match std::io::stdin().lock().read_line(&mut zeile) {
|
||||
Ok(0) | Err(_) => {
|
||||
self.eof = true;
|
||||
self.puffer.push_back(Ereignis::Ende);
|
||||
}
|
||||
Ok(_) => {
|
||||
while zeile.ends_with('\n') || zeile.ends_with('\r') {
|
||||
zeile.pop();
|
||||
}
|
||||
for c in zeile.chars() {
|
||||
self.puffer.push_back(Ereignis::Taste(c.to_string(), 0));
|
||||
}
|
||||
self.puffer.push_back(Ereignis::Taste(
|
||||
tb_runtime::host::taste::ENTER.to_string(),
|
||||
0,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Host for PipeHost {
|
||||
fn present(&mut self, _screen: &tb_runtime::screen::TextScreen) {}
|
||||
|
||||
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
|
||||
if self.puffer.is_empty() && blockierend {
|
||||
self.nachfuellen();
|
||||
}
|
||||
self.puffer.pop_front()
|
||||
}
|
||||
|
||||
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
|
||||
if let Some(event) = self.next_event(false) {
|
||||
return Some(event);
|
||||
}
|
||||
if let Some(deadline) = deadline_ms {
|
||||
std::thread::sleep(std::time::Duration::from_millis(
|
||||
deadline.saturating_sub(self.jetzt_ms()),
|
||||
));
|
||||
None
|
||||
} else {
|
||||
self.next_event(true).or(Some(Ereignis::Ende))
|
||||
}
|
||||
}
|
||||
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.start.elapsed().as_millis() as u64
|
||||
}
|
||||
#[cfg(test)]
|
||||
fn run_chain(
|
||||
args: &[String],
|
||||
host: &mut dyn tb_runtime::host::Host,
|
||||
size: Option<(usize, usize)>,
|
||||
) -> Result<(tb_vm::interp::RunEvent, tb_vm::interp::Vm), ExitCode> {
|
||||
let first = args.first().ok_or(ExitCode::from(1))?;
|
||||
tb_runner::run_chain(
|
||||
Path::new(first),
|
||||
&args[1..].join(" "),
|
||||
|path| compile(Some(&path.display().to_string())).map(|(_, module)| module),
|
||||
host,
|
||||
size,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -329,3 +329,58 @@ fn designer_frm_is_accepted_by_the_standalone_cli_compiler() {
|
||||
tb_vm::project_io::load_program(&dir.join("designed.tbc")).unwrap();
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_build_optionen_sind_explizit_und_tbc_bleibt_standard() {
|
||||
let dir = std::env::temp_dir().join(format!("tb-native-options-{}", std::process::id()));
|
||||
std::fs::create_dir(&dir).unwrap();
|
||||
struct Cleanup(std::path::PathBuf);
|
||||
impl Drop for Cleanup {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
let _cleanup = Cleanup(dir.clone());
|
||||
std::fs::write(dir.join("main.bas"), "PRINT 42\nEND\n").unwrap();
|
||||
assert!(tbc(&dir, "build", "main.bas").status.success());
|
||||
let original = std::fs::read(dir.join("main.tbc")).unwrap();
|
||||
assert_eq!(&original[..4], b"TBC\0");
|
||||
for (options, diagnostic) in [
|
||||
(vec!["--bogus"], "Unbekannte Build-Option"),
|
||||
(vec!["--exe", "--target"], "Wert fehlt"),
|
||||
(
|
||||
vec!["--exe", "--target", "x86_64-apple-darwin"],
|
||||
"Nicht unterstütztes Target",
|
||||
),
|
||||
(vec!["--exe", "--exe"], "Doppeltes --exe"),
|
||||
(vec!["--output", "program"], "benötigen --exe"),
|
||||
(
|
||||
vec!["--exe", "--template", "missing", "-o", "program"],
|
||||
"Runtime-Vorlage fehlt",
|
||||
),
|
||||
(
|
||||
vec![
|
||||
"--exe",
|
||||
"--target",
|
||||
"aarch64-apple-darwin",
|
||||
"--target",
|
||||
"aarch64-apple-darwin",
|
||||
],
|
||||
"Doppelte Option",
|
||||
),
|
||||
] {
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_tbc"))
|
||||
.current_dir(&dir)
|
||||
.args(["build", "main.bas"])
|
||||
.args(options)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(out.status.code(), Some(1), "{out:?}");
|
||||
assert!(
|
||||
String::from_utf8_lossy(&out.stderr).contains(diagnostic),
|
||||
"{out:?}"
|
||||
);
|
||||
assert_eq!(std::fs::read(dir.join("main.tbc")).unwrap(), original);
|
||||
assert!(!dir.join("program").exists());
|
||||
}
|
||||
}
|
||||
|
||||
9
crates/tb-export/Cargo.toml
Normal file
9
crates/tb-export/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "tb-export"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tb-vm.workspace = true
|
||||
anyhow.workspace = true
|
||||
12
crates/tb-export/src/bin/tb-template.rs
Normal file
12
crates/tb-export/src/bin/tb-template.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! Builder-Hilfe: geprüfte Metadaten für eine frisch gebaute Runtime schreiben.
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
anyhow::ensure!(
|
||||
args.len() == 2,
|
||||
"Aufruf: tb-template <tbrt-Pfad> <Target-Triple>"
|
||||
);
|
||||
tb_export::prepare_template(
|
||||
std::path::Path::new(&args[0]),
|
||||
tb_export::Target::parse(&args[1])?,
|
||||
)
|
||||
}
|
||||
514
crates/tb-export/src/lib.rs
Normal file
514
crates/tb-export/src/lib.rs
Normal file
@@ -0,0 +1,514 @@
|
||||
//! Geprüfte Runtime-Vorlagen, Nutzlastcontainer und geschützte Veröffentlichung.
|
||||
use anyhow::{bail, ensure, Context, Result};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
use tb_vm::bytecode::{CompiledModule, TBC_VERSION};
|
||||
|
||||
pub const RUNTIME_VERSION: u32 = 1;
|
||||
const CONTAINER_VERSION: u32 = 1;
|
||||
const MAGIC: &[u8; 8] = b"TBPCODE!";
|
||||
const FOOTER: usize = 48;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
pub enum Target {
|
||||
WindowsAmd64 = 1,
|
||||
MacosArm64 = 2,
|
||||
LinuxAmd64 = 3,
|
||||
LinuxArm64 = 4,
|
||||
}
|
||||
impl Target {
|
||||
pub const ALL: [Self; 4] = [
|
||||
Self::WindowsAmd64,
|
||||
Self::MacosArm64,
|
||||
Self::LinuxAmd64,
|
||||
Self::LinuxArm64,
|
||||
];
|
||||
pub fn triple(self) -> &'static str {
|
||||
match self {
|
||||
Self::WindowsAmd64 => "x86_64-pc-windows-msvc",
|
||||
Self::MacosArm64 => "aarch64-apple-darwin",
|
||||
Self::LinuxAmd64 => "x86_64-unknown-linux-gnu",
|
||||
Self::LinuxArm64 => "aarch64-unknown-linux-gnu",
|
||||
}
|
||||
}
|
||||
pub fn parse(s: &str) -> Result<Self> {
|
||||
Self::ALL
|
||||
.into_iter()
|
||||
.find(|t| t.triple() == s)
|
||||
.with_context(|| format!("Nicht unterstütztes Target: {s}"))
|
||||
}
|
||||
pub fn host() -> Result<Self> {
|
||||
match (std::env::consts::OS, std::env::consts::ARCH) {
|
||||
("windows", "x86_64") => Ok(Self::WindowsAmd64),
|
||||
("macos", "aarch64") => Ok(Self::MacosArm64),
|
||||
("linux", "x86_64") => Ok(Self::LinuxAmd64),
|
||||
("linux", "aarch64") => Ok(Self::LinuxArm64),
|
||||
other => bail!("Kein unterstütztes Host-Target: {other:?}"),
|
||||
}
|
||||
}
|
||||
pub fn runtime_name(self) -> &'static str {
|
||||
if self == Self::WindowsAmd64 {
|
||||
"tbrt.exe"
|
||||
} else {
|
||||
"tbrt"
|
||||
}
|
||||
}
|
||||
}
|
||||
fn part(b: &[u8], at: usize, len: usize) -> Result<&[u8]> {
|
||||
b.get(at..at.checked_add(len).context("Längenüberlauf")?)
|
||||
.context("Abgeschnittenes natives Format/Nutzlast")
|
||||
}
|
||||
fn u16_at(b: &[u8], at: usize) -> Result<u16> {
|
||||
Ok(u16::from_le_bytes(part(b, at, 2)?.try_into()?))
|
||||
}
|
||||
fn u32_at(b: &[u8], at: usize) -> Result<u32> {
|
||||
Ok(u32::from_le_bytes(part(b, at, 4)?.try_into()?))
|
||||
}
|
||||
fn u64_at(b: &[u8], at: usize) -> Result<u64> {
|
||||
Ok(u64::from_le_bytes(part(b, at, 8)?.try_into()?))
|
||||
}
|
||||
fn size_at(b: &[u8], at: usize) -> Result<usize> {
|
||||
usize::try_from(u64_at(b, at)?).context("Nutzlastlänge nicht darstellbar")
|
||||
}
|
||||
fn put64(b: &mut [u8], at: usize, n: usize) {
|
||||
b[at..at + 8].copy_from_slice(&(n as u64).to_le_bytes());
|
||||
}
|
||||
|
||||
/// FNV-1a-64 gegen Übertragungs-/Dateibeschädigung; keine Herkunftsauthentisierung.
|
||||
pub fn checksum(b: &[u8]) -> u64 {
|
||||
b.iter().fold(0xcbf29ce484222325, |h, v| {
|
||||
(h ^ u64::from(*v)).wrapping_mul(0x100000001b3)
|
||||
})
|
||||
}
|
||||
|
||||
fn macho_commands(b: &[u8]) -> Result<Vec<(u32, usize)>> {
|
||||
let end = 32usize
|
||||
.checked_add(u32_at(b, 20)? as usize)
|
||||
.context("Mach-O-Headerlänge")?;
|
||||
part(b, 0, end)?;
|
||||
let mut at = 32;
|
||||
let mut commands = Vec::new();
|
||||
for _ in 0..u32_at(b, 16)? {
|
||||
ensure!(at + 8 <= end, "Ungültige Mach-O-Ladekommandos");
|
||||
let cmd = u32_at(b, at)?;
|
||||
let len = u32_at(b, at + 4)? as usize;
|
||||
ensure!(
|
||||
len >= 8 && len.is_multiple_of(8) && len <= end - at,
|
||||
"Ungültige Mach-O-Kommandolänge"
|
||||
);
|
||||
commands.push((cmd, at));
|
||||
at += len;
|
||||
}
|
||||
ensure!(at == end, "Mach-O-Kommandotabelle unvollständig");
|
||||
Ok(commands)
|
||||
}
|
||||
|
||||
/// Format-/Architekturprüfung; synthetische Header sind kein Zielausführungsnachweis.
|
||||
pub fn native_target(b: &[u8]) -> Result<Target> {
|
||||
part(b, 0, 64)?;
|
||||
if b.starts_with(b"MZ") {
|
||||
let pe = u32_at(b, 0x3c)? as usize;
|
||||
ensure!(part(b, pe, 4)? == b"PE\0\0", "Ungültiges PE-Format");
|
||||
ensure!(
|
||||
u16_at(b, pe + 4)? == 0x8664,
|
||||
"PE-Architektur muss amd64 sein"
|
||||
);
|
||||
ensure!(
|
||||
u16_at(b, pe + 24)? == 0x20b && u16_at(b, pe + 92)? == 3,
|
||||
"PE muss PE32+ Terminalanwendung sein"
|
||||
);
|
||||
ensure!(
|
||||
u16_at(b, pe + 22)? & 0x2002 == 2,
|
||||
"PE muss Executable statt DLL sein"
|
||||
);
|
||||
return Ok(Target::WindowsAmd64);
|
||||
}
|
||||
if b.starts_with(b"\x7fELF") {
|
||||
ensure!(
|
||||
matches!(b[7], 0 | 3),
|
||||
"ELF-System-ABI muss System V/Linux sein"
|
||||
);
|
||||
ensure!(
|
||||
part(b, 4, 3)? == [2, 1, 1],
|
||||
"ELF muss 64-Bit Little Endian sein"
|
||||
);
|
||||
ensure!(matches!(u16_at(b, 16)?, 2 | 3), "ELF muss ausführbar sein");
|
||||
part(b, 0, 64)?;
|
||||
return match u16_at(b, 18)? {
|
||||
62 => Ok(Target::LinuxAmd64),
|
||||
183 => Ok(Target::LinuxArm64),
|
||||
_ => bail!("Nicht unterstützte ELF-Architektur"),
|
||||
};
|
||||
}
|
||||
if b.starts_with(&[0xcf, 0xfa, 0xed, 0xfe]) {
|
||||
ensure!(
|
||||
u32_at(b, 4)? == 0x0100000c,
|
||||
"Mach-O-Architektur muss arm64 sein"
|
||||
);
|
||||
ensure!(u32_at(b, 12)? == 2, "Mach-O muss Executable sein");
|
||||
macho_commands(b)?;
|
||||
return Ok(Target::MacosArm64);
|
||||
}
|
||||
bail!("Unbekanntes natives Zielformat (PE/ELF/Mach-O erwartet)")
|
||||
}
|
||||
|
||||
/// Mach-O-Signaturen folgen der Nutzlast. Ihr Ladekommando ist die stabile Grenze.
|
||||
fn content_end(b: &[u8], target: Target) -> Result<usize> {
|
||||
if target == Target::MacosArm64 {
|
||||
let signatures: Vec<_> = macho_commands(b)?
|
||||
.into_iter()
|
||||
.filter(|(cmd, _)| *cmd == 0x1d)
|
||||
.collect();
|
||||
ensure!(signatures.len() <= 1, "Doppelte Mach-O-Signatur");
|
||||
if let Some((_, at)) = signatures.first() {
|
||||
ensure!(u32_at(b, at + 4)? == 16, "Ungültiges Signaturkommando");
|
||||
let off = u32_at(b, at + 8)? as usize;
|
||||
let len = u32_at(b, at + 12)? as usize;
|
||||
part(b, off, len)?;
|
||||
ensure!(off + len == b.len(), "Daten hinter Mach-O-Signatur");
|
||||
return Ok(off);
|
||||
}
|
||||
}
|
||||
Ok(b.len())
|
||||
}
|
||||
|
||||
pub fn embedded(b: &[u8], expected: Target) -> Result<CompiledModule> {
|
||||
ensure!(
|
||||
native_target(b)? == expected,
|
||||
"Executable-Target passt nicht zum Host"
|
||||
);
|
||||
let end = content_end(b, expected)?;
|
||||
// codesign richtet die Signatur auf 16 Bytes aus; ausschließlich Nullpadding zulassen.
|
||||
let footer = (0..=15)
|
||||
.find_map(|padding| {
|
||||
let at = end.checked_sub(FOOTER + padding)?;
|
||||
(b.get(at..at + 8) == Some(MAGIC) && b[at + FOOTER..end].iter().all(|v| *v == 0))
|
||||
.then_some(at)
|
||||
})
|
||||
.context("Fehlende oder beschädigte eingebettete Nutzlast (leere tbrt-Vorlage)")?;
|
||||
ensure!(
|
||||
u32_at(b, footer + 8)? == CONTAINER_VERSION,
|
||||
"Inkompatible Container-Version"
|
||||
);
|
||||
ensure!(
|
||||
u32_at(b, footer + 12)? == RUNTIME_VERSION,
|
||||
"Inkompatible Runtime-Version"
|
||||
);
|
||||
ensure!(
|
||||
u32_at(b, footer + 16)? == u32::from(TBC_VERSION),
|
||||
"Inkompatible TBC-Version"
|
||||
);
|
||||
ensure!(
|
||||
u32_at(b, footer + 20)? == expected as u32,
|
||||
"Nutzlast-Target passt nicht zum Executable"
|
||||
);
|
||||
let offset = size_at(b, footer + 24)?;
|
||||
let len = size_at(b, footer + 32)?;
|
||||
ensure!(
|
||||
offset >= 64 && offset <= footer && len == footer - offset,
|
||||
"Ungültige Nutzlastgrenzen"
|
||||
);
|
||||
let payload = part(b, offset, len)?;
|
||||
ensure!(
|
||||
checksum(payload) == u64_at(b, footer + 40)?,
|
||||
"Nutzlast-Prüfsumme stimmt nicht"
|
||||
);
|
||||
CompiledModule::from_tbc(payload)
|
||||
.map_err(|e| anyhow::anyhow!("Eingebettetes Programm nicht ladbar: {e}"))
|
||||
}
|
||||
|
||||
/// Im nativen Runner vorhanden; verhindert Verwechslung mit beliebigen Executables.
|
||||
pub const fn runtime_marker(target: Target) -> [u8; 32] {
|
||||
let mut marker = [0; 32];
|
||||
let magic = *b"TBRT-RUNTIME-v1!";
|
||||
let mut i = 0;
|
||||
while i < 16 {
|
||||
marker[i] = magic[i];
|
||||
i += 1;
|
||||
}
|
||||
let values = [
|
||||
CONTAINER_VERSION,
|
||||
RUNTIME_VERSION,
|
||||
TBC_VERSION as u32,
|
||||
target as u32,
|
||||
];
|
||||
i = 0;
|
||||
while i < 4 {
|
||||
let bytes = values[i].to_le_bytes();
|
||||
let mut j = 0;
|
||||
while j < 4 {
|
||||
marker[16 + i * 4 + j] = bytes[j];
|
||||
j += 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
marker
|
||||
}
|
||||
fn check_runner(b: &[u8], target: Target) -> Result<()> {
|
||||
ensure!(
|
||||
b.windows(32).any(|w| w == runtime_marker(target)),
|
||||
"Keine kompatible tbrt-Runtime-Vorlage (Runtime-/Format-/Target-Markierung fehlt)"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn manifest(b: &[u8], target: Target) -> String {
|
||||
format!("TBRT-TEMPLATE 1\nruntime={RUNTIME_VERSION}\npackage={}\ntbc={TBC_VERSION}\ntarget={}\nchecksum={:016x}\n",env!("CARGO_PKG_VERSION"),target.triple(),checksum(b))
|
||||
}
|
||||
pub fn manifest_path(path: &Path) -> PathBuf {
|
||||
let mut p = path.as_os_str().to_owned();
|
||||
p.push(".meta");
|
||||
PathBuf::from(p)
|
||||
}
|
||||
/// Für den Runtime-Builder; kein impliziter Build beim Export.
|
||||
pub fn prepare_template(path: &Path, target: Target) -> Result<()> {
|
||||
let b = fs::read(path)?;
|
||||
check_runner(&b, target)?;
|
||||
ensure!(
|
||||
native_target(&b)? == target,
|
||||
"Vorlagen-Zielformat/Architektur widerspricht {}",
|
||||
target.triple()
|
||||
);
|
||||
ensure!(
|
||||
embedded(&b, target).is_err(),
|
||||
"Bereits eingebettetes Programm ist keine leere Vorlage"
|
||||
);
|
||||
publish(
|
||||
&manifest_path(path),
|
||||
false,
|
||||
&[path.to_path_buf()],
|
||||
&|| false,
|
||||
|temp| {
|
||||
fs::write(temp, manifest(&b, target))?;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
fn template(path: &Path, target: Target) -> Result<Vec<u8>> {
|
||||
let b = fs::read(path).with_context(|| format!("Runtime-Vorlage fehlt: {}", path.display()))?;
|
||||
ensure!(
|
||||
native_target(&b)? == target,
|
||||
"Vorlagen-Zielformat/Architektur passt nicht zu {}",
|
||||
target.triple()
|
||||
);
|
||||
let found = fs::read_to_string(manifest_path(path)).context("Vorlagenmetadaten fehlen")?;
|
||||
check_runner(&b, target)?;
|
||||
let want = manifest(&b, target);
|
||||
for (index, (a, z)) in found.lines().zip(want.lines()).enumerate() {
|
||||
ensure!(
|
||||
a == z,
|
||||
"Inkompatible Vorlagenmetadaten, Zeile {}: Soll {z}, Ist {a}",
|
||||
index + 1
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
found == want,
|
||||
"Unvollständige Vorlagenmetadaten oder Integritätsdaten"
|
||||
);
|
||||
Ok(b)
|
||||
}
|
||||
|
||||
static NEXT: AtomicU64 = AtomicU64::new(0);
|
||||
struct Temporary(PathBuf);
|
||||
impl Drop for Temporary {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(&self.0);
|
||||
}
|
||||
}
|
||||
fn destination(path: &Path, protected: &[PathBuf]) -> Result<Option<Vec<u8>>> {
|
||||
let key = tb_vm::project_io::identity(path)?;
|
||||
for source in protected {
|
||||
ensure!(
|
||||
tb_vm::project_io::identity(source)? != key,
|
||||
"Ausgabe würde Projektdaten/Vorlage überschreiben: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
match fs::symlink_metadata(path) {
|
||||
Ok(meta) => {
|
||||
ensure!(
|
||||
meta.is_file() && !meta.permissions().readonly(),
|
||||
"Ziel ist keine beschreibbare reguläre Datei: {}",
|
||||
path.display()
|
||||
);
|
||||
Ok(Some(fs::read(path)?))
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
/// No-clobber per hard_link; Ersetzen nur nach expliziter Freigabe und unverändertem Ziel.
|
||||
/// prepare darf finalisieren; cancellation wird vor Arbeit und Veröffentlichung geprüft.
|
||||
pub fn publish(
|
||||
path: &Path,
|
||||
overwrite: bool,
|
||||
protected: &[PathBuf],
|
||||
cancelled: &dyn Fn() -> bool,
|
||||
prepare: impl FnOnce(&Path) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
ensure!(!cancelled(), "Export abgebrochen");
|
||||
let initial = destination(path, protected)?;
|
||||
ensure!(
|
||||
initial.is_none() || overwrite,
|
||||
"Ziel existiert; --force zum Ersetzen: {}",
|
||||
path.display()
|
||||
);
|
||||
let parent = path
|
||||
.parent()
|
||||
.filter(|p| !p.as_os_str().is_empty())
|
||||
.unwrap_or(Path::new("."));
|
||||
let temporary = loop {
|
||||
let p = parent.join(format!(
|
||||
".tb-export-{}-{}.tmp",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
match fs::OpenOptions::new().write(true).create_new(true).open(&p) {
|
||||
Ok(_) => break Temporary(p),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
};
|
||||
prepare(&temporary.0)?;
|
||||
fs::File::open(&temporary.0)?.sync_all()?;
|
||||
ensure!(!cancelled(), "Export abgebrochen");
|
||||
ensure!(
|
||||
destination(path, protected)? == initial,
|
||||
"Ziel wurde während des Exports verändert"
|
||||
);
|
||||
if initial.is_some() {
|
||||
fs::rename(&temporary.0, path)?;
|
||||
} else {
|
||||
fs::hard_link(&temporary.0, path)
|
||||
.context("Ziel inzwischen belegt oder Veröffentlichung nicht möglich")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn codesign(path: &Path, args: &[&str]) -> Result<()> {
|
||||
ensure!(
|
||||
cfg!(target_os = "macos"),
|
||||
"macOS-Finalisierung benötigt macOS mit /usr/bin/codesign"
|
||||
);
|
||||
let result = std::process::Command::new("/usr/bin/codesign")
|
||||
.args(args)
|
||||
.arg(path)
|
||||
.output()
|
||||
.context("Finalisierung: /usr/bin/codesign fehlt oder ist nicht ausführbar")?;
|
||||
ensure!(
|
||||
result.status.success(),
|
||||
"Finalisierung fehlgeschlagen: {}",
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
fn append_payload(b: &mut Vec<u8>, module: &CompiledModule, target: Target) -> Result<()> {
|
||||
module.validate().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
if target == Target::WindowsAmd64 {
|
||||
let pe = u32_at(b, 0x3c)? as usize;
|
||||
// Eine bestehende Authenticode-Signatur wäre nach dem Einbetten ungültig.
|
||||
if u32_at(b, pe + 132)? > 4 {
|
||||
ensure!(
|
||||
part(b, pe + 168, 8)?.iter().all(|v| *v == 0),
|
||||
"Signierte PE-Vorlagen werden nicht unterstützt; unsigned tbrt benötigt"
|
||||
);
|
||||
}
|
||||
// Für Benutzerprogramme optional; keine veraltete PE-Prüfsumme übernehmen.
|
||||
b[pe + 88..pe + 92].fill(0);
|
||||
}
|
||||
let payload = module.to_tbc();
|
||||
let offset = b.len();
|
||||
b.extend_from_slice(&payload);
|
||||
b.extend_from_slice(MAGIC);
|
||||
for n in [
|
||||
CONTAINER_VERSION,
|
||||
RUNTIME_VERSION,
|
||||
u32::from(TBC_VERSION),
|
||||
target as u32,
|
||||
] {
|
||||
b.extend_from_slice(&n.to_le_bytes());
|
||||
}
|
||||
for n in [offset as u64, payload.len() as u64, checksum(&payload)] {
|
||||
b.extend_from_slice(&n.to_le_bytes());
|
||||
}
|
||||
if target == Target::MacosArm64 {
|
||||
let link = macho_commands(b)?
|
||||
.into_iter()
|
||||
.find(|(cmd, at)| {
|
||||
*cmd == 0x19 && b.get(at + 8..at + 24) == Some(b"__LINKEDIT\0\0\0\0\0\0")
|
||||
})
|
||||
.context("Mach-O __LINKEDIT fehlt")?
|
||||
.1;
|
||||
ensure!(
|
||||
u32_at(b, link + 4)? as usize >= 72,
|
||||
"Mach-O-Segment zu kurz"
|
||||
);
|
||||
let start = size_at(b, link + 40)?;
|
||||
ensure!(start <= offset, "Ungültige __LINKEDIT-Grenze");
|
||||
let len = b.len() - start;
|
||||
put64(b, link + 48, len);
|
||||
put64(
|
||||
b,
|
||||
link + 32,
|
||||
len.checked_add(16383).context("Mach-O-Länge")? & !16383,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Export<'a> {
|
||||
pub module: &'a CompiledModule,
|
||||
pub target: Target,
|
||||
pub template: &'a Path,
|
||||
pub output: &'a Path,
|
||||
pub overwrite: bool,
|
||||
pub protected: &'a [PathBuf],
|
||||
}
|
||||
pub fn export(request: Export<'_>, cancelled: &dyn Fn() -> bool) -> Result<()> {
|
||||
let mut b = template(request.template, request.target)?;
|
||||
let mut protected = request.protected.to_vec();
|
||||
protected.extend([
|
||||
request.template.to_path_buf(),
|
||||
manifest_path(request.template),
|
||||
]);
|
||||
protected.extend(
|
||||
request
|
||||
.module
|
||||
.sources
|
||||
.iter()
|
||||
.map(|s| PathBuf::from(&s.path)),
|
||||
);
|
||||
publish(
|
||||
request.output,
|
||||
request.overwrite,
|
||||
&protected,
|
||||
cancelled,
|
||||
|temp| {
|
||||
if request.target == Target::MacosArm64 {
|
||||
fs::write(temp, &b)?;
|
||||
codesign(temp, &["--remove-signature"])?;
|
||||
b = fs::read(temp)?;
|
||||
}
|
||||
append_payload(&mut b, request.module, request.target)?;
|
||||
fs::write(temp, &b)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(temp, fs::Permissions::from_mode(0o755))?;
|
||||
}
|
||||
if request.target == Target::MacosArm64 {
|
||||
codesign(temp, &["--force", "--sign", "-", "--timestamp=none"])?;
|
||||
codesign(temp, &["--verify", "--strict"])?;
|
||||
}
|
||||
embedded(&fs::read(temp)?, request.target)?;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
226
crates/tb-export/src/tests.rs
Normal file
226
crates/tb-export/src/tests.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use super::*;
|
||||
use std::cell::Cell;
|
||||
|
||||
fn header(t: Target) -> Vec<u8> {
|
||||
let mut b = vec![0; 256];
|
||||
match t {
|
||||
Target::WindowsAmd64 => {
|
||||
b[..2].copy_from_slice(b"MZ");
|
||||
b[0x3c..0x40].copy_from_slice(&64u32.to_le_bytes());
|
||||
b[64..68].copy_from_slice(b"PE\0\0");
|
||||
b[68..70].copy_from_slice(&0x8664u16.to_le_bytes());
|
||||
b[86..88].copy_from_slice(&2u16.to_le_bytes());
|
||||
b[88..90].copy_from_slice(&0x20bu16.to_le_bytes());
|
||||
b[156..158].copy_from_slice(&3u16.to_le_bytes());
|
||||
}
|
||||
Target::MacosArm64 => {
|
||||
b[..4].copy_from_slice(&[0xcf, 0xfa, 0xed, 0xfe]);
|
||||
b[4..8].copy_from_slice(&0x0100000cu32.to_le_bytes());
|
||||
b[12..16].copy_from_slice(&2u32.to_le_bytes());
|
||||
}
|
||||
Target::LinuxAmd64 | Target::LinuxArm64 => {
|
||||
b[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
|
||||
b[16..18].copy_from_slice(&2u16.to_le_bytes());
|
||||
let cpu: u16 = if t == Target::LinuxAmd64 { 62 } else { 183 };
|
||||
b[18..20].copy_from_slice(&cpu.to_le_bytes());
|
||||
}
|
||||
}
|
||||
b.extend_from_slice(&runtime_marker(t));
|
||||
b
|
||||
}
|
||||
struct Dir(PathBuf);
|
||||
impl Dir {
|
||||
fn new() -> Self {
|
||||
let p = std::env::temp_dir().join(format!(
|
||||
"tb-export-test-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::create_dir(&p).unwrap();
|
||||
Self(p)
|
||||
}
|
||||
}
|
||||
impl Drop for Dir {
|
||||
fn drop(&mut self) {
|
||||
fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn targets_versions_integrity_and_payload_bounds_are_enforced() {
|
||||
let d = Dir::new();
|
||||
for t in Target::ALL {
|
||||
let b = header(t);
|
||||
assert_eq!(native_target(&b).unwrap(), t);
|
||||
let path = d.0.join(t.runtime_name());
|
||||
fs::write(&path, &b).unwrap();
|
||||
prepare_template(&path, t).unwrap();
|
||||
assert_eq!(template(&path, t).unwrap(), b);
|
||||
let other = if t == Target::LinuxAmd64 {
|
||||
Target::LinuxArm64
|
||||
} else {
|
||||
Target::LinuxAmd64
|
||||
};
|
||||
assert!(template(&path, other)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Architektur"));
|
||||
let meta = manifest(&b, t);
|
||||
for wrong in [
|
||||
meta.replace("runtime=1", "runtime=2"),
|
||||
meta.replace("package=0.1.0", "package=9"),
|
||||
meta.replace("tbc=4", "tbc=99"),
|
||||
meta.replace("checksum=", "checksum=0"),
|
||||
] {
|
||||
fs::write(manifest_path(&path), wrong).unwrap();
|
||||
assert!(template(&path, t).is_err());
|
||||
}
|
||||
fs::remove_file(manifest_path(&path)).unwrap();
|
||||
assert!(template(&path, t).is_err());
|
||||
fs::remove_file(&path).unwrap();
|
||||
assert!(template(&path, t)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("fehlt"));
|
||||
for len in 0..64 {
|
||||
assert!(native_target(&b[..len]).is_err());
|
||||
}
|
||||
}
|
||||
assert!(Target::parse("aarch64-pc-windows-msvc").is_err());
|
||||
assert!(Target::parse("x86_64-apple-darwin").is_err());
|
||||
let mut foreign_abi = header(Target::LinuxAmd64);
|
||||
foreign_abi[7] = 9;
|
||||
assert!(native_target(&foreign_abi).is_err());
|
||||
let module = tb_vm::compile_source("TEST", "PRINT 42\nEND\n").unwrap();
|
||||
for t in [Target::WindowsAmd64, Target::LinuxAmd64, Target::LinuxArm64] {
|
||||
let mut b = header(t);
|
||||
if t == Target::WindowsAmd64 {
|
||||
let mut signed = b.clone();
|
||||
signed[196..200].copy_from_slice(&5u32.to_le_bytes());
|
||||
signed[232] = 1;
|
||||
assert!(append_payload(&mut signed, &module, t)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Signierte PE"));
|
||||
b[152..156].copy_from_slice(&123u32.to_le_bytes());
|
||||
}
|
||||
append_payload(&mut b, &module, t).unwrap();
|
||||
if t == Target::WindowsAmd64 {
|
||||
assert_eq!(&b[152..156], &[0; 4]);
|
||||
}
|
||||
assert_eq!(embedded(&b, t).unwrap().to_tbc(), module.to_tbc());
|
||||
let at = b.len() - FOOTER;
|
||||
for field in [8, 12, 16, 20, 24, 32, 40] {
|
||||
let mut bad = b.clone();
|
||||
bad[at + field] ^= 0xff;
|
||||
assert!(embedded(&bad, t).is_err(), "{field}");
|
||||
}
|
||||
for cut in 1..=FOOTER + 1 {
|
||||
assert!(embedded(&b[..b.len() - cut], t).is_err());
|
||||
}
|
||||
let mut bad = b.clone();
|
||||
bad[300] ^= 1;
|
||||
assert!(embedded(&bad, t).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn publication_preserves_originals_on_conflict_failure_race_and_cancel() {
|
||||
let d = Dir::new();
|
||||
let out = d.0.join("result");
|
||||
let source = d.0.join("source.bas");
|
||||
fs::write(&source, b"source").unwrap();
|
||||
fs::write(&out, b"old").unwrap();
|
||||
assert!(publish(&out, false, &[], &|| false, |_| panic!(
|
||||
"darf nicht schreiben"
|
||||
))
|
||||
.is_err());
|
||||
assert!(publish(
|
||||
&source,
|
||||
true,
|
||||
std::slice::from_ref(&source),
|
||||
&|| false,
|
||||
|_| panic!("Projektschutz")
|
||||
)
|
||||
.is_err());
|
||||
assert!(publish(&out, true, &[], &|| false, |p| {
|
||||
fs::write(p, b"new")?;
|
||||
bail!("Finalisierungsfehler")
|
||||
})
|
||||
.is_err());
|
||||
assert_eq!(fs::read(&out).unwrap(), b"old");
|
||||
let cancel = Cell::new(false);
|
||||
assert!(publish(&out, true, &[], &|| cancel.get(), |p| {
|
||||
fs::write(p, b"new")?;
|
||||
cancel.set(true);
|
||||
Ok(())
|
||||
})
|
||||
.is_err());
|
||||
assert_eq!(fs::read(&out).unwrap(), b"old");
|
||||
assert!(publish(&out, true, &[], &|| true, |_| panic!("abgebrochen")).is_err());
|
||||
assert!(publish(&out, true, &[], &|| false, |p| {
|
||||
fs::write(p, b"new")?;
|
||||
fs::write(&out, b"external")?;
|
||||
Ok(())
|
||||
})
|
||||
.is_err());
|
||||
assert_eq!(fs::read(&out).unwrap(), b"external");
|
||||
fs::remove_file(&out).unwrap();
|
||||
assert!(publish(&out, true, &[], &|| false, |p| {
|
||||
fs::write(p, b"new")?;
|
||||
fs::write(&out, b"racer")?;
|
||||
Ok(())
|
||||
})
|
||||
.is_err());
|
||||
assert_eq!(fs::read(&out).unwrap(), b"racer");
|
||||
assert!(
|
||||
publish(&d.0.join("missing/result"), false, &[], &|| false, |_| Ok(
|
||||
()
|
||||
))
|
||||
.is_err()
|
||||
);
|
||||
publish(&out, true, &[], &|| false, |p| {
|
||||
fs::write(p, b"approved")?;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(fs::read(&out).unwrap(), b"approved");
|
||||
assert_eq!(fs::read(&source).unwrap(), b"source");
|
||||
assert_eq!(fs::read_dir(&d.0).unwrap().count(), 2);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::os::unix::fs::symlink(&source, d.0.join("alias")).unwrap();
|
||||
assert!(
|
||||
publish(&d.0.join("alias"), true, &[], &|| false, |_| panic!(
|
||||
"Symlink"
|
||||
))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fehlgeschlagene_native_finalisierung_erhaelt_das_ziel() {
|
||||
let dir = Dir::new();
|
||||
let runtime = dir.0.join("tbrt");
|
||||
let out = dir.0.join("program");
|
||||
fs::write(&runtime, header(Target::MacosArm64)).unwrap();
|
||||
prepare_template(&runtime, Target::MacosArm64).unwrap();
|
||||
fs::write(&out, b"original").unwrap();
|
||||
let module = tb_vm::compile_source("TEST", "END\n").unwrap();
|
||||
let error = export(
|
||||
Export {
|
||||
module: &module,
|
||||
target: Target::MacosArm64,
|
||||
template: &runtime,
|
||||
output: &out,
|
||||
overwrite: true,
|
||||
protected: &[],
|
||||
},
|
||||
&|| false,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("Finalisierung"), "{error:#}");
|
||||
assert_eq!(fs::read(&out).unwrap(), b"original");
|
||||
assert_eq!(fs::read_dir(&dir.0).unwrap().count(), 3);
|
||||
}
|
||||
11
crates/tb-runner/Cargo.toml
Normal file
11
crates/tb-runner/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "tb-runner"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tb-vm.workspace = true
|
||||
tb-runtime.workspace = true
|
||||
tb-export.workspace = true
|
||||
tb-ui = { workspace = true, features = ["terminal"] }
|
||||
58
crates/tb-runner/src/bin/tbrt.rs
Normal file
58
crates/tb-runner/src/bin/tbrt.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
//! Native Runtime: kein IDE- oder BASIC-Quellcompiler-Einstieg.
|
||||
use std::process::ExitCode;
|
||||
#[used]
|
||||
static MARKER: [u8; 32] = tb_export::runtime_marker(if cfg!(target_os = "windows") {
|
||||
tb_export::Target::WindowsAmd64
|
||||
} else if cfg!(target_os = "macos") {
|
||||
tb_export::Target::MacosArm64
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
tb_export::Target::LinuxArm64
|
||||
} else {
|
||||
tb_export::Target::LinuxAmd64
|
||||
});
|
||||
fn main() -> ExitCode {
|
||||
std::hint::black_box(&MARKER);
|
||||
let path = match std::env::current_exe() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Executable nicht auffindbar: {e}");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
let bytes = match std::fs::read(&path) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
eprintln!("Executable nicht lesbar: {e}");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
let target = match tb_export::Target::host() {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
if let Err(e) = tb_export::embedded(&bytes, target) {
|
||||
eprintln!("Ladefehler: {e:#}");
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
let command = std::env::args().skip(1).collect::<Vec<_>>().join(" ");
|
||||
tb_runner::run(&path, &command, |current| {
|
||||
let result = if current == path {
|
||||
tb_export::embedded(&bytes, target).map_err(|e| format!("{e:#}"))
|
||||
} else if tb_vm::project_io::has_extension(current, "tbc") {
|
||||
std::fs::read(current)
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|b| {
|
||||
tb_vm::bytecode::CompiledModule::from_tbc(&b).map_err(|e| e.to_string())
|
||||
})
|
||||
} else {
|
||||
Err("tbrt benötigt ein vorkompiliertes externes RUN-Ziel (.tbc); kein BASIC-Quellcompiler enthalten".into())
|
||||
};
|
||||
result.map_err(|e| {
|
||||
eprintln!("{}: Ladefehler: {e}", current.display());
|
||||
ExitCode::from(1)
|
||||
})
|
||||
})
|
||||
}
|
||||
204
crates/tb-runner/src/lib.rs
Normal file
204
crates/tb-runner/src/lib.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
//! Gemeinsamer CLI-/Standalone-Runner; der Aufrufer bestimmt das Ladeverfahren.
|
||||
use std::path::Path;
|
||||
use std::process::ExitCode;
|
||||
use tb_runtime::host::{Ereignis, Host};
|
||||
use tb_ui::host::TerminalHost;
|
||||
use tb_vm::bytecode::CompiledModule;
|
||||
use tb_vm::interp::{RunEvent, Vm};
|
||||
use tb_vm::project_io::{new_execution, run_target};
|
||||
|
||||
pub fn run(
|
||||
first: &Path,
|
||||
command: &str,
|
||||
mut load: impl FnMut(&Path) -> Result<CompiledModule, ExitCode>,
|
||||
) -> ExitCode {
|
||||
let result = match TerminalHost::new() {
|
||||
Ok(mut host) => {
|
||||
let size = host.groesse().ok();
|
||||
let result = run_chain(first, command, &mut load, &mut host, size);
|
||||
drop(host);
|
||||
result
|
||||
}
|
||||
Err(_) => run_chain(first, command, &mut load, &mut PipeHost::new(), None),
|
||||
};
|
||||
finish(result)
|
||||
}
|
||||
|
||||
fn finish(result: Result<(RunEvent, Vm), ExitCode>) -> ExitCode {
|
||||
let (ereignis, vm) = match result {
|
||||
Ok(result) => result,
|
||||
Err(code) => return code,
|
||||
};
|
||||
print!("{}", tb_runtime::snapshot::text(&vm.rt.screen));
|
||||
// `LPRINT` sammelt im Druckerpuffer; am Programmende geht er in die
|
||||
// Datei LPT1.TXT im aktuellen Verzeichnis (dokumentierte Abweichung —
|
||||
// einen Druckerkanal gibt es plattformübergreifend nicht).
|
||||
if !vm.rt.print.drucker.is_empty() {
|
||||
if let Err(e) = std::fs::write("LPT1.TXT", &vm.rt.print.drucker) {
|
||||
eprintln!("Druckerausgabe nicht schreibbar: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
match ereignis {
|
||||
RunEvent::Ended => ExitCode::SUCCESS,
|
||||
RunEvent::Stopped { line } => {
|
||||
// STOP außerhalb der IDE: Meldung + Exit-Code ≠ 0 (D6).
|
||||
eprintln!("{}:{line}: STOP in line {line}", vm.current_file());
|
||||
ExitCode::from(3)
|
||||
}
|
||||
RunEvent::Error {
|
||||
code,
|
||||
line,
|
||||
message,
|
||||
} => {
|
||||
eprintln!(
|
||||
"{}:{line}:{}: Runtime error {code}: {message} in line {line}",
|
||||
vm.current_file(),
|
||||
vm.current_source_pos().column
|
||||
);
|
||||
ExitCode::from(2)
|
||||
}
|
||||
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
||||
RunEvent::Interrupted { line } => {
|
||||
eprintln!("Abgebrochen in Zeile {line}");
|
||||
ExitCode::from(3)
|
||||
}
|
||||
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
||||
RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } => ExitCode::from(2),
|
||||
RunEvent::Restart { .. } => unreachable!("RUN wird vom Runner aufgelöst"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_chain(
|
||||
first: &Path,
|
||||
command: &str,
|
||||
mut load: impl FnMut(&Path) -> Result<CompiledModule, ExitCode>,
|
||||
host: &mut dyn Host,
|
||||
size: Option<(usize, usize)>,
|
||||
) -> Result<(RunEvent, Vm), ExitCode> {
|
||||
let mut current = first.to_path_buf();
|
||||
let mut start_line = None;
|
||||
loop {
|
||||
let module = load(¤t)?;
|
||||
let mut vm = new_execution(module, command, size, start_line.take()).map_err(|error| {
|
||||
eprintln!("Runtime error {}: {}", error.0, error);
|
||||
ExitCode::from(2)
|
||||
})?;
|
||||
if !vm.rt.zeitpunkt().1 {
|
||||
eprintln!("Zeitzone nicht ermittelbar — Zeitfunktionen rechnen in UTC.");
|
||||
}
|
||||
let event = vm.run(host);
|
||||
let event =
|
||||
if event == RunEvent::Ended && vm.forms.has_visible_forms() && !vm.is_terminated() {
|
||||
vm.run_visible_forms(host)
|
||||
} else {
|
||||
event
|
||||
};
|
||||
match event {
|
||||
RunEvent::Restart { program, line } => {
|
||||
if let Some(program) = program {
|
||||
current = run_target(¤t, &program).map_err(|error| {
|
||||
eprintln!("{error}");
|
||||
ExitCode::from(1)
|
||||
})?;
|
||||
}
|
||||
start_line = line;
|
||||
}
|
||||
event => return Ok((event, vm)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Host ohne Terminal: für Pipes und Skripte (`tbc run x.bas < eingabe.txt`).
|
||||
/// Zeigt während des Laufs nichts an; die Ausgabe entsteht am Ende aus dem
|
||||
/// Bildschirm-Snapshot. Tastendrücke kommen zeilenweise von stdin.
|
||||
struct PipeHost {
|
||||
puffer: std::collections::VecDeque<Ereignis>,
|
||||
eof: bool,
|
||||
start: std::time::Instant,
|
||||
}
|
||||
|
||||
impl PipeHost {
|
||||
fn new() -> Self {
|
||||
let mut host = PipeHost {
|
||||
puffer: std::collections::VecDeque::new(),
|
||||
eof: false,
|
||||
start: std::time::Instant::now(),
|
||||
};
|
||||
use std::io::{IsTerminal, Read};
|
||||
let mut stdin = std::io::stdin();
|
||||
if !stdin.is_terminal() {
|
||||
let mut input = String::new();
|
||||
let _ = stdin.read_to_string(&mut input);
|
||||
for line in input.split_inclusive('\n') {
|
||||
for character in line.trim_end_matches(['\r', '\n']).chars() {
|
||||
host.puffer
|
||||
.push_back(Ereignis::Taste(character.to_string(), 0));
|
||||
}
|
||||
host.puffer.push_back(Ereignis::Taste(
|
||||
tb_runtime::host::taste::ENTER.to_string(),
|
||||
0,
|
||||
));
|
||||
}
|
||||
host.puffer.push_back(Ereignis::Ende);
|
||||
host.eof = true;
|
||||
}
|
||||
host
|
||||
}
|
||||
|
||||
/// Eine Zeile von stdin in Tastendrücke zerlegen.
|
||||
fn nachfuellen(&mut self) {
|
||||
use std::io::BufRead;
|
||||
if self.eof {
|
||||
return;
|
||||
}
|
||||
let mut zeile = String::new();
|
||||
match std::io::stdin().lock().read_line(&mut zeile) {
|
||||
Ok(0) | Err(_) => {
|
||||
self.eof = true;
|
||||
self.puffer.push_back(Ereignis::Ende);
|
||||
}
|
||||
Ok(_) => {
|
||||
while zeile.ends_with('\n') || zeile.ends_with('\r') {
|
||||
zeile.pop();
|
||||
}
|
||||
for c in zeile.chars() {
|
||||
self.puffer.push_back(Ereignis::Taste(c.to_string(), 0));
|
||||
}
|
||||
self.puffer.push_back(Ereignis::Taste(
|
||||
tb_runtime::host::taste::ENTER.to_string(),
|
||||
0,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Host for PipeHost {
|
||||
fn present(&mut self, _screen: &tb_runtime::screen::TextScreen) {}
|
||||
|
||||
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
|
||||
if self.puffer.is_empty() && blockierend {
|
||||
self.nachfuellen();
|
||||
}
|
||||
self.puffer.pop_front()
|
||||
}
|
||||
|
||||
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
|
||||
if let Some(event) = self.next_event(false) {
|
||||
return Some(event);
|
||||
}
|
||||
if let Some(deadline) = deadline_ms {
|
||||
std::thread::sleep(std::time::Duration::from_millis(
|
||||
deadline.saturating_sub(self.jetzt_ms()),
|
||||
));
|
||||
None
|
||||
} else {
|
||||
self.next_event(true).or(Some(Ereignis::Ende))
|
||||
}
|
||||
}
|
||||
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.start.elapsed().as_millis() as u64
|
||||
}
|
||||
}
|
||||
@@ -849,9 +849,14 @@ fn link(
|
||||
Ok((result, debug_maps))
|
||||
}
|
||||
|
||||
// Erst ein ausdrücklich erzeugter DebugCompiler bindet den Quellcompiler ein.
|
||||
// Die reine Runtime kann dadurch den gleichen VM-Pfad ohne Parser/Codegenerator linken.
|
||||
type DebugCompileFn = fn(&DebugCompiler, u16, &str, &str, bool) -> Result<DebugCode, String>;
|
||||
|
||||
/// Ephemeral symbol/link context, deliberately not part of TBC serialization.
|
||||
#[derive(Clone)]
|
||||
pub struct DebugCompiler {
|
||||
compile_fn: DebugCompileFn,
|
||||
modules: Vec<(Module, FormCatalog, DebugMap)>,
|
||||
symbols: tb_frontend::sema::DebugSymbols,
|
||||
slots: Vec<u16>,
|
||||
@@ -914,6 +919,7 @@ impl ProjectCompiler {
|
||||
}
|
||||
}
|
||||
DebugCompiler {
|
||||
compile_fn: DebugCompiler::compile_impl,
|
||||
modules,
|
||||
symbols,
|
||||
slots,
|
||||
@@ -928,6 +934,15 @@ impl DebugCompiler {
|
||||
procedure: &str,
|
||||
text: &str,
|
||||
expression: bool,
|
||||
) -> Result<DebugCode, String> {
|
||||
(self.compile_fn)(self, module, procedure, text, expression)
|
||||
}
|
||||
fn compile_impl(
|
||||
&self,
|
||||
module: u16,
|
||||
procedure: &str,
|
||||
text: &str,
|
||||
expression: bool,
|
||||
) -> Result<DebugCode, String> {
|
||||
if let Some(error) = &self.error {
|
||||
return Err(error.clone());
|
||||
|
||||
Reference in New Issue
Block a user