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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user