Phase 6: P-Code-Bibliotheken und Linker abschließen, Cross-Buildplan festhalten

This commit is contained in:
2026-09-07 16:06:23 +02:00
parent 60d37ec79d
commit 25176947ba
33 changed files with 2416 additions and 203 deletions

View File

@@ -21,11 +21,12 @@ fn main() -> ExitCode {
match args.first().map(String::as_str) {
Some("run") => cmd_run(&args[1..]),
Some("build") => cmd_build(&args[1..]),
Some("link") => cmd_link(&args[1..]),
Some("check") => cmd_check(&args[1..]),
Some("convert-frm") => cmd_convert_frm(&args[1..]),
_ => {
eprintln!(
"Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak|datei.tbc> | tbc convert-frm <quelle.frm> <ziel.frm>"
"Aufruf: tbc run|build|link|check <datei.bas|datei.frm|projekt.mak|datei.tbc> | tbc convert-frm <quelle.frm> <ziel.frm>"
);
ExitCode::from(1)
}
@@ -79,7 +80,7 @@ fn compile(
path_arg: Option<&String>,
) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
let Some(path) = path_arg else {
eprintln!("Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak|datei.tbc>");
eprintln!("Aufruf: tbc run|build|link|check <datei.bas|datei.frm|projekt.mak|datei.tbc>");
return Err(ExitCode::from(1));
};
let path = PathBuf::from(path);
@@ -118,6 +119,10 @@ fn compile(
}
fn cmd_check(args: &[String]) -> ExitCode {
if args.len() != 1 {
eprintln!("Aufruf: tbc check <Projekt>");
return ExitCode::from(1);
}
match compile(args.first()) {
Ok(_) => ExitCode::SUCCESS,
Err(code) => code,
@@ -125,7 +130,13 @@ fn cmd_check(args: &[String]) -> ExitCode {
}
fn cmd_build(args: &[String]) -> ExitCode {
match build(args) {
cmd_output(args, false)
}
fn cmd_link(args: &[String]) -> ExitCode {
cmd_output(args, true)
}
fn cmd_output(args: &[String], link: bool) -> ExitCode {
match build(args, link) {
Ok(path) => {
println!("{}", path.display());
ExitCode::SUCCESS
@@ -137,9 +148,10 @@ fn cmd_build(args: &[String]) -> ExitCode {
}
}
fn build(args: &[String]) -> anyhow::Result<PathBuf> {
fn build(args: &[String], link: bool) -> anyhow::Result<PathBuf> {
use anyhow::{bail, ensure, Context};
let mut source = None;
let mut inputs = Vec::new();
let mut library = false;
let mut exe = false;
let mut force = false;
let mut output = None;
@@ -148,6 +160,13 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
let mut args = args.iter();
while let Some(arg) = args.next() {
match arg.as_str() {
"--library" => {
ensure!(
!library && !link,
"--library ist nur bei build zulässig und darf nicht doppelt sein"
);
library = true;
}
"--exe" => {
ensure!(!exe, "Doppeltes --exe");
exe = true;
@@ -171,30 +190,74 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
"Doppelte Option {arg}"
);
}
"--" => {
for value in args.by_ref() {
ensure!(
source.replace(value.clone()).is_none(),
"Mehrere Quelldateien angegeben"
);
}
}
"--" => inputs.extend(args.by_ref().cloned()),
value if value.starts_with('-') => bail!("Unbekannte Build-Option: {value}"),
_ => {
ensure!(
source.replace(arg.clone()).is_none(),
"Mehrere Quelldateien angegeben"
);
}
_ => inputs.push(arg.clone()),
}
}
let source = source.context(
"Aufruf: tbc build <Quelle> [--exe --target <Triple> --template <tbrt> -o <Ziel> --force]",
)?;
ensure!(!inputs.is_empty(), "Aufruf: tbc build|link <Quelle> [Bibliothek.tbl ...] [--library | --exe] [-o Ziel] [--force]");
ensure!(!(exe && library), "--library und --exe schließen sich aus");
ensure!(
exe || (target.is_none() && template.is_none() && output.is_none() && !force),
"Native Ausgabeoptionen benötigen --exe"
exe || (target.is_none() && template.is_none()),
"--target/--template benötigen --exe"
);
ensure!(
exe || library || link || output.is_none(),
"Ausgabeoptionen benötigen --exe, --library oder link"
);
let source = &inputs[0];
let path = PathBuf::from(source);
let tbc = tb_vm::project_io::has_extension(&path, "tbc");
ensure!(!tbc || (!library && inputs.len()==1 && (!link || exe)), "Fertiges TBC kann nur ohne zusätzliche TBL mit link --exe verpackt werden; es ist kein Library-Produkt");
let mut protected = vec![path.clone()];
let mut compiler = tb_vm::project::ProjectCompiler::default();
let loaded = if tbc {
None
} else {
let mut project = SourceLoader::default()
.load(&path)
.map_err(anyhow::Error::msg)?;
for input in &inputs[1..] {
project
.add_library(Path::new(input))
.map_err(anyhow::Error::msg)?;
}
protected.extend(project.protected_inputs());
Some(project)
};
let has_libraries = loaded.as_ref().is_some_and(|p| !p.libraries.is_empty());
let diags = |errors: Vec<tb_frontend::Diagnostic>| {
anyhow::anyhow!(errors
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n"))
};
let cancel = export_cancel::ExportCancel::new()?;
if library {
let product = loaded
.as_ref()
.unwrap()
.compile_library(&mut compiler)
.map_err(diags)?;
let bytes = product.to_tbl().map_err(anyhow::Error::msg)?;
let out = output
.map(PathBuf::from)
.unwrap_or_else(|| path.with_extension("tbl"));
tb_export::publish(
&out,
force,
&protected,
&|| cancel.cancelled(),
|temporary| {
std::fs::write(temporary, &bytes)?;
tb_vm::library::Library::from_tbl(&std::fs::read(temporary)?)
.map_err(anyhow::Error::msg)?;
Ok(())
},
)?;
return Ok(out);
}
let selected = if exe {
Some(match target {
Some(s) => tb_export::Target::parse(&s)?,
@@ -203,8 +266,14 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
} else {
None
};
let (path, module) =
compile(Some(&source)).map_err(|_| anyhow::anyhow!("Build fehlgeschlagen"))?;
let module = if let Some(project) = loaded {
project
.compile(&mut compiler, &module_name(&path))
.map_err(diags)?
} else {
tb_vm::bytecode::CompiledModule::from_tbc(&std::fs::read(&path)?)
.map_err(|e| anyhow::anyhow!("{e}"))?
};
if let Some(target) = selected {
let out = output.map(PathBuf::from).unwrap_or_else(|| {
path.with_extension(if target == tb_export::Target::WindowsAmd64 {
@@ -222,7 +291,6 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
.join(target.triple())
.join(target.runtime_name()),
};
let cancel = export_cancel::ExportCancel::new()?;
tb_export::export(
tb_export::Export {
module: &module,
@@ -230,14 +298,37 @@ fn build(args: &[String]) -> anyhow::Result<PathBuf> {
template: &template,
output: &out,
overwrite: force,
protected: &[path],
protected: &protected,
},
&|| cancel.cancelled(),
)?;
Ok(out)
} else {
let out = path.with_extension("tbc");
std::fs::write(&out, module.to_tbc())?;
let legacy = !link && !has_libraries && output.is_none() && !force && inputs.len() == 1;
let out = output
.map(PathBuf::from)
.unwrap_or_else(|| path.with_extension("tbc"));
if legacy
&& !tbc
&& protected.iter().all(|p| {
tb_vm::project_io::identity(p).ok() != tb_vm::project_io::identity(&out).ok()
})
{
std::fs::write(&out, module.to_tbc())?;
} else {
tb_export::publish(
&out,
force,
&protected,
&|| cancel.cancelled(),
|temporary| {
std::fs::write(temporary, module.to_tbc())?;
tb_vm::bytecode::CompiledModule::from_tbc(&std::fs::read(temporary)?)
.map_err(|e| anyhow::anyhow!("{e}"))?;
Ok(())
},
)?;
}
Ok(out)
}
}

View File

@@ -384,3 +384,92 @@ fn native_build_optionen_sind_explizit_und_tbc_bleibt_standard() {
assert!(!dir.join("program").exists());
}
}
#[test]
fn library_cli_links_without_sources_and_protects_all_inputs_and_outputs() {
let dir = std::env::temp_dir().join(format!("tb-library-cli-{}", 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());
let call = |args: &[&str]| {
Command::new(env!("CARGO_BIN_EXE_tbc"))
.current_dir(&dir)
.env("PATH", "")
.args(args)
.output()
.unwrap()
};
let success = |args: &[&str]| {
let out = call(args);
assert!(out.status.success(), "{args:?}: {out:?}");
out
};
std::fs::write(
dir.join("a.bas"),
"DECLARE SUB Second(n%)\nSUB First(n%)\nSecond n%\nEND SUB\n",
)
.unwrap();
std::fs::write(dir.join("b.bas"), "SUB Second(n%)\nn%=n%+7\nEND SUB\n").unwrap();
success(&["build", "a.bas", "--library"]);
success(&["build", "b.bas", "--library", "--output", "b.tbl"]);
std::fs::remove_file(dir.join("a.bas")).unwrap();
std::fs::remove_file(dir.join("b.bas")).unwrap();
std::fs::write(dir.join("main.bas"), "CALL First(n%)\nPRINT n%\nEND\n").unwrap();
assert!(!call(&["link", "main.bas", "a.tbl"]).status.success());
success(&["link", "main.bas", "a.tbl", "b.tbl", "-o", "linked.tbc"]);
assert_eq!(success(&["run", "linked.tbc"]).stdout, b" 7 \n");
std::fs::write(dir.join("app.mak"), "main.bas\na.tbl\nb.tbl\n").unwrap();
success(&["check", "app.mak"]);
success(&["build", "app.mak"]);
assert_eq!(success(&["run", "app.mak"]).stdout, b" 7 \n");
let before = std::fs::read(dir.join("a.tbl")).unwrap();
let old = std::fs::read(dir.join("linked.tbc")).unwrap();
for args in [
vec![
"link", "main.bas", "a.tbl", "b.tbl", "-o", "a.tbl", "--force",
],
vec!["link", "main.bas", "a.tbl", "b.tbl", "-o", "linked.tbc"],
vec!["build", "main.bas", "--library", "--exe"],
vec![
"build",
"main.bas",
"--library",
"--target",
"aarch64-apple-darwin",
],
vec!["link", "linked.tbc", "a.tbl", "--exe"],
vec!["link", "linked.tbc"],
vec!["run", "a.tbl"],
vec!["check", "a.tbl"],
vec!["link", "a.tbl"],
vec!["link", "main.bas", "main.bas"],
vec!["link", "main.bas", "a.tbl", "a.tbl"],
vec!["link", "app.mak", "-o", "app.mak", "--force"],
vec!["link", "app.mak", "-o", "missing/output.tbc"],
] {
assert!(!call(&args).status.success(), "accepted {args:?}");
assert_eq!(std::fs::read(dir.join("a.tbl")).unwrap(), before);
assert_eq!(std::fs::read(dir.join("linked.tbc")).unwrap(), old);
}
success(&["link", "app.mak", "-o", "linked.tbc", "--force"]);
let old = std::fs::read(dir.join("linked.tbc")).unwrap();
std::fs::write(dir.join("a.tbl"), &before[..before.len() - 1]).unwrap();
assert!(!call(&["link", "app.mak", "-o", "linked.tbc", "--force"])
.status
.success());
assert_eq!(std::fs::read(dir.join("linked.tbc")).unwrap(), old);
for p in ["a.tbl", "b.tbl", "main.bas", "app.mak"] {
std::fs::remove_file(dir.join(p)).unwrap();
}
assert_eq!(success(&["run", "linked.tbc"]).stdout, b" 7 \n");
assert!(!std::fs::read_dir(&dir).unwrap().any(|e| e
.unwrap()
.file_name()
.to_string_lossy()
.contains(".tmp")));
}