Phase 6: IDE-Exportanbindung abschließen und Change archivieren
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -644,6 +644,7 @@ dependencies = [
|
||||
"crossterm",
|
||||
"pulldown-cmark",
|
||||
"ratatui",
|
||||
"tb-export",
|
||||
"tb-frontend",
|
||||
"tb-runtime",
|
||||
"tb-ui",
|
||||
|
||||
@@ -81,7 +81,9 @@ Native Programme lassen sich mit `tbc build --exe` aus vorgebauten Runtime-Vorla
|
||||
erzeugen. Aufrufe, Zielvoraussetzungen und Abnahme: [Native Executables](docs/native-executables.md).
|
||||
|
||||
Mit `tbc build --library` entstehen portable `.tbl`-Bibliotheken; `tbc link`
|
||||
verbindet sie mit einem Hauptprojekt zu TBC oder einem Executable.
|
||||
verbindet sie mit einem Hauptprojekt zu TBC oder einem Executable. Die IDE
|
||||
nutzt denselben Dienst für Make Library und Make EXE, einschließlich
|
||||
ungespeicherter Puffer, TBL-Projektmitgliedern und abbrechbarer Erzeugung.
|
||||
[Bibliotheksformat, Befehle und Abnahme](docs/pcode-bibliotheken.md).
|
||||
|
||||
## Lizenz
|
||||
|
||||
@@ -283,21 +283,25 @@ fn create(a: &mut App, t: &Temp) -> PathBuf {
|
||||
path
|
||||
}
|
||||
fn exports(a: &mut App, t: &Temp) {
|
||||
a.export_backend = false;
|
||||
let stamp = ProjectStamp::capture(&a.project);
|
||||
for c in [Command::MakeExe, Command::MakeLibrary] {
|
||||
menu(a, c);
|
||||
assert!(draw(a).contains("Erzeugen: Phase 6"));
|
||||
let output = if c == Command::MakeLibrary { 1 } else { 3 };
|
||||
if c == Command::MakeExe {
|
||||
choice(a, 1, "linux");
|
||||
choice(a, 2, "x86_64");
|
||||
field(a, 3, "");
|
||||
}
|
||||
field(a, output, "");
|
||||
plain(a, K::Enter);
|
||||
assert!(a.dialog.as_ref().unwrap().error.contains("Ausgabepfad"));
|
||||
field(a, 3, &t.0.join("bad.tbc").display().to_string());
|
||||
field(a, output, &t.0.join("bad.tbc").display().to_string());
|
||||
plain(a, K::Enter);
|
||||
assert!(a.dialog.as_ref().unwrap().error.contains("TBC"));
|
||||
let existing = t.0.join("existing");
|
||||
let existing = t.0.join("existing.tbl");
|
||||
fs::write(&existing, "original").unwrap();
|
||||
field(a, 3, &existing.display().to_string());
|
||||
field(a, output, &existing.display().to_string());
|
||||
plain(a, K::Enter);
|
||||
assert!(a
|
||||
.dialog
|
||||
@@ -309,8 +313,8 @@ fn exports(a: &mut App, t: &Temp) {
|
||||
plain(a, K::Char(' '));
|
||||
plain(a, K::Enter);
|
||||
assert_eq!(fs::read_to_string(existing).unwrap(), "original");
|
||||
let out = t.0.join("native-result");
|
||||
field(a, 3, &out.display().to_string());
|
||||
let out = t.0.join("native-result.tbl");
|
||||
field(a, output, &out.display().to_string());
|
||||
plain(a, K::Enter);
|
||||
let request = a.dialog.as_ref().unwrap().request.clone().unwrap();
|
||||
assert_eq!(request.project, stamp);
|
||||
@@ -805,3 +809,157 @@ fn reference_matrix_is_complete_and_points_to_runnable_tests() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_export(a: &mut App) {
|
||||
let start = std::time::Instant::now();
|
||||
while a.dialog.as_ref().unwrap().export_status == ExportStatus::Running {
|
||||
a.tick_export();
|
||||
assert!(start.elapsed().as_secs() < 15, "Export hängt");
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
}
|
||||
assert_eq!(
|
||||
a.dialog.as_ref().unwrap().export_status,
|
||||
ExportStatus::Success,
|
||||
"{:?}",
|
||||
a.dialog
|
||||
);
|
||||
plain(a, K::Esc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Echter Zielnachweis: TB_IDE_RUNTIME_DIR muss geprüfte Host-tbrt-Vorlage enthalten"]
|
||||
fn real_native_ide_exports_and_source_free_cli_library_consumer() {
|
||||
let runtime_dir =
|
||||
PathBuf::from(std::env::var_os("TB_IDE_RUNTIME_DIR").expect("TB_IDE_RUNTIME_DIR fehlt"));
|
||||
let target = tb_export::Target::host().unwrap();
|
||||
let t = Temp::new();
|
||||
let mut a = App::new(&t.0, t.0.join("options"), (100, 30)).unwrap();
|
||||
a.runtime_dir = runtime_dir;
|
||||
let _project = create(&mut a, &t);
|
||||
a.project.open_document(&t.0.join("work.bi")).unwrap();
|
||||
let disk: Vec<_> = a
|
||||
.project
|
||||
.documents()
|
||||
.map(|(_, d)| {
|
||||
(
|
||||
d.source_path().to_path_buf(),
|
||||
fs::read(d.source_path()).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let include = a.project.find_document(&t.0.join("work.bi")).unwrap();
|
||||
a.project.replace_text(include, 6..7, "7").unwrap();
|
||||
let stamp = ProjectStamp::capture(&a.project);
|
||||
// The real project still supports debugging before export.
|
||||
open_code(&mut a, "Form1.frm");
|
||||
plain(&mut a, K::F(8));
|
||||
tick(&mut a, 0);
|
||||
assert_eq!(a.execution, Execution::Paused, "{}", a.message);
|
||||
|
||||
let exe = t.0.join(if cfg!(windows) {
|
||||
"program.exe"
|
||||
} else {
|
||||
"program"
|
||||
});
|
||||
menu(&mut a, Command::MakeExe);
|
||||
let (system, arch) = tb_ide::export::target_parts(target);
|
||||
choice(&mut a, 1, system);
|
||||
choice(&mut a, 2, arch);
|
||||
field(&mut a, 3, &exe.display().to_string());
|
||||
plain(&mut a, K::Enter);
|
||||
finish_export(&mut a);
|
||||
assert_eq!(
|
||||
tb_export::native_target(&fs::read(&exe).unwrap()).unwrap(),
|
||||
target
|
||||
);
|
||||
let library = t.0.join("library.tbl");
|
||||
menu(&mut a, Command::MakeLibrary);
|
||||
field(&mut a, 1, &library.display().to_string());
|
||||
click_hit(&mut a, |h| matches!(h, Hit::DialogSubmit));
|
||||
finish_export(&mut a);
|
||||
let mut compiler = tb_vm::project::ProjectCompiler::default();
|
||||
compiler.debug_symbols = true;
|
||||
let expected = a
|
||||
.project
|
||||
.sources()
|
||||
.unwrap()
|
||||
.compile_library(&mut compiler)
|
||||
.unwrap()
|
||||
.to_tbl()
|
||||
.unwrap();
|
||||
assert_eq!(fs::read(&library).unwrap(), expected);
|
||||
assert_eq!(ProjectStamp::capture(&a.project), stamp);
|
||||
assert!(a.project.document(include).unwrap().is_dirty());
|
||||
for (path, bytes) in disk {
|
||||
assert_eq!(fs::read(&path).unwrap(), bytes);
|
||||
fs::remove_file(path).unwrap();
|
||||
}
|
||||
fs::remove_file(t.0.join("project.mak")).unwrap();
|
||||
let consumer = t.0.join("consumer.bas");
|
||||
fs::write(
|
||||
&consumer,
|
||||
"DIM n AS INTEGER\nn=8\nCALL Bump(n)\nPRINT n\nEND\n",
|
||||
)
|
||||
.unwrap();
|
||||
let tbc = t.0.join("consumer.tbc");
|
||||
// Exactly the tbc link command implementation, with the library sources gone.
|
||||
build(
|
||||
&[
|
||||
consumer.display().to_string(),
|
||||
library.display().to_string(),
|
||||
"-o".into(),
|
||||
tbc.display().to_string(),
|
||||
],
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let cli = tb_vm::project_io::load_program(&tbc).unwrap();
|
||||
fs::write(t.0.join("consumer.mak"), "consumer.bas\nlibrary.tbl\n").unwrap();
|
||||
let mut other = App::new(&t.0, t.0.join("other-options"), (100, 30)).unwrap();
|
||||
other
|
||||
.load_initial_project(t.0.join("consumer.mak"))
|
||||
.unwrap();
|
||||
other.start_execution().unwrap();
|
||||
tick(&mut other, 0);
|
||||
assert_eq!(snapshot::text(other.session.screen()), " 9 \n");
|
||||
let mut cli_vm = new_execution(cli, "", None, None).unwrap();
|
||||
let mut host = tb_runtime::host::CaptureHost::default();
|
||||
assert!(matches!(cli_vm.run(&mut host), RunEvent::Ended));
|
||||
assert_eq!(
|
||||
snapshot::text(&cli_vm.rt.screen),
|
||||
snapshot::text(other.session.screen())
|
||||
);
|
||||
fs::remove_file(&library).unwrap();
|
||||
fs::remove_file(&tbc).unwrap();
|
||||
fs::remove_file(&consumer).unwrap();
|
||||
use std::io::Write;
|
||||
let mut child = std::process::Command::new(&exe)
|
||||
.arg("!")
|
||||
.current_dir(&t.0)
|
||||
.env("PATH", "")
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
child.stdin.take().unwrap().write_all(b"abc\n").unwrap();
|
||||
let start = std::time::Instant::now();
|
||||
while child.try_wait().unwrap().is_none() {
|
||||
if start.elapsed().as_secs() > 10 {
|
||||
let _ = child.kill();
|
||||
panic!("Natives IDE-Programm hängt");
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
let result = child.wait_with_output().unwrap();
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
let output = String::from_utf8_lossy(&result.stdout).replace("\r\n", "\n");
|
||||
assert!(output.contains("8 abc!"), "{output:?}");
|
||||
assert!(fs::read_to_string(t.0.join("result.txt"))
|
||||
.unwrap()
|
||||
.contains('8'));
|
||||
}
|
||||
|
||||
@@ -345,6 +345,46 @@ pub fn publish(
|
||||
cancelled: &dyn Fn() -> bool,
|
||||
prepare: impl FnOnce(&Path) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
prepare_publication(path, overwrite, protected, cancelled, prepare)?.publish(cancelled)
|
||||
}
|
||||
|
||||
/// Fertiges Staging-Ergebnis. Drop verwirft es; nur der Auftragseigner veröffentlicht.
|
||||
pub struct PreparedPublication {
|
||||
path: PathBuf,
|
||||
protected: Vec<PathBuf>,
|
||||
initial: Option<Vec<u8>>,
|
||||
temporary: Temporary,
|
||||
}
|
||||
impl PreparedPublication {
|
||||
pub fn publish(self, cancelled: &dyn Fn() -> bool) -> Result<()> {
|
||||
let Self {
|
||||
path,
|
||||
protected,
|
||||
initial,
|
||||
temporary,
|
||||
} = self;
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prepare_publication(
|
||||
path: &Path,
|
||||
overwrite: bool,
|
||||
protected: &[PathBuf],
|
||||
cancelled: &dyn Fn() -> bool,
|
||||
prepare: impl FnOnce(&Path) -> Result<()>,
|
||||
) -> Result<PreparedPublication> {
|
||||
ensure!(!cancelled(), "Export abgebrochen");
|
||||
let initial = destination(path, protected)?;
|
||||
ensure!(
|
||||
@@ -371,29 +411,38 @@ pub fn publish(
|
||||
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(())
|
||||
Ok(PreparedPublication {
|
||||
path: path.into(),
|
||||
protected: protected.into(),
|
||||
initial,
|
||||
temporary,
|
||||
})
|
||||
}
|
||||
|
||||
fn codesign(path: &Path, args: &[&str]) -> Result<()> {
|
||||
fn codesign(path: &Path, args: &[&str], cancelled: &dyn Fn() -> bool) -> Result<()> {
|
||||
ensure!(
|
||||
cfg!(target_os = "macos"),
|
||||
"macOS-Finalisierung benötigt macOS mit /usr/bin/codesign"
|
||||
);
|
||||
let result = std::process::Command::new("/usr/bin/codesign")
|
||||
let mut child = std::process::Command::new("/usr/bin/codesign")
|
||||
.args(args)
|
||||
.arg(path)
|
||||
.output()
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.context("Finalisierung: /usr/bin/codesign fehlt oder ist nicht ausführbar")?;
|
||||
loop {
|
||||
if cancelled() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
bail!("Export abgebrochen");
|
||||
}
|
||||
if child.try_wait()?.is_some() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
let result = child.wait_with_output()?;
|
||||
ensure!(
|
||||
result.status.success(),
|
||||
"Finalisierung fehlgeschlagen: {}",
|
||||
@@ -464,6 +513,12 @@ pub struct Export<'a> {
|
||||
pub protected: &'a [PathBuf],
|
||||
}
|
||||
pub fn export(request: Export<'_>, cancelled: &dyn Fn() -> bool) -> Result<()> {
|
||||
prepare_export(request, cancelled)?.publish(cancelled)
|
||||
}
|
||||
pub fn prepare_export(
|
||||
request: Export<'_>,
|
||||
cancelled: &dyn Fn() -> bool,
|
||||
) -> Result<PreparedPublication> {
|
||||
let mut b = template(request.template, request.target)?;
|
||||
let mut protected = request.protected.to_vec();
|
||||
protected.extend([
|
||||
@@ -477,7 +532,7 @@ pub fn export(request: Export<'_>, cancelled: &dyn Fn() -> bool) -> Result<()> {
|
||||
.iter()
|
||||
.map(|s| PathBuf::from(&s.path)),
|
||||
);
|
||||
publish(
|
||||
prepare_publication(
|
||||
request.output,
|
||||
request.overwrite,
|
||||
&protected,
|
||||
@@ -485,7 +540,7 @@ pub fn export(request: Export<'_>, cancelled: &dyn Fn() -> bool) -> Result<()> {
|
||||
|temp| {
|
||||
if request.target == Target::MacosArm64 {
|
||||
fs::write(temp, &b)?;
|
||||
codesign(temp, &["--remove-signature"])?;
|
||||
codesign(temp, &["--remove-signature"], cancelled)?;
|
||||
b = fs::read(temp)?;
|
||||
}
|
||||
append_payload(&mut b, request.module, request.target)?;
|
||||
@@ -496,8 +551,12 @@ pub fn export(request: Export<'_>, cancelled: &dyn Fn() -> bool) -> Result<()> {
|
||||
fs::set_permissions(temp, fs::Permissions::from_mode(0o755))?;
|
||||
}
|
||||
if request.target == Target::MacosArm64 {
|
||||
codesign(temp, &["--force", "--sign", "-", "--timestamp=none"])?;
|
||||
codesign(temp, &["--verify", "--strict"])?;
|
||||
codesign(
|
||||
temp,
|
||||
&["--force", "--sign", "-", "--timestamp=none"],
|
||||
cancelled,
|
||||
)?;
|
||||
codesign(temp, &["--verify", "--strict"], cancelled)?;
|
||||
}
|
||||
embedded(&fs::read(temp)?, request.target)?;
|
||||
Ok(())
|
||||
|
||||
@@ -13,6 +13,7 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
tb-frontend.workspace = true
|
||||
tb-vm.workspace = true
|
||||
tb-export.workspace = true
|
||||
tb-runtime.workspace = true
|
||||
tb-ui = { workspace = true, features = ["terminal"] }
|
||||
ratatui.workspace = true
|
||||
|
||||
@@ -301,6 +301,9 @@ pub struct App {
|
||||
pub last_command: Option<Command>,
|
||||
pub hits: Vec<(Rect, Hit)>,
|
||||
pub basic_events: Vec<Event>,
|
||||
pub export_backend: bool,
|
||||
pub runtime_dir: PathBuf,
|
||||
pub(crate) export_job: Option<crate::export::Job>,
|
||||
pub last_export: Option<(ExportRequest, ExportStatus)>,
|
||||
pub config_path: PathBuf,
|
||||
pub saved_options: Options,
|
||||
@@ -351,6 +354,12 @@ impl App {
|
||||
last_command: None,
|
||||
hits: vec![],
|
||||
basic_events: vec![],
|
||||
export_backend: true,
|
||||
runtime_dir: std::env::current_exe()?
|
||||
.parent()
|
||||
.unwrap_or(base)
|
||||
.join("runtimes"),
|
||||
export_job: None,
|
||||
last_export: None,
|
||||
next_window: 1,
|
||||
geometry_action: None,
|
||||
@@ -674,14 +683,18 @@ impl App {
|
||||
SaveAs => self.save_dialog(false, vec![id.unwrap()], AfterSave::Stay)?,
|
||||
SaveProject => self.save_dialog(
|
||||
true,
|
||||
self.project.documents().map(|(id, _)| id).collect(),
|
||||
self.project
|
||||
.documents()
|
||||
.filter(|(_, d)| !d.is_library())
|
||||
.map(|(id, _)| id)
|
||||
.collect(),
|
||||
AfterSave::Stay,
|
||||
)?,
|
||||
RemoveFile => {
|
||||
let ids = self.project.members();
|
||||
let names = self.names(&ids);
|
||||
let mut replacements = vec!["Bisheriger Standard".into()];
|
||||
replacements.extend(names.clone());
|
||||
replacements.extend(self.names(&self.project.source_members()));
|
||||
self.open_dialog(
|
||||
"Remove File",
|
||||
DialogKind::Remove,
|
||||
@@ -692,7 +705,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
Startup => {
|
||||
let ids = self.project.members();
|
||||
let ids = self.project.source_members();
|
||||
let mut names = vec!["Bisheriger Standard".into()];
|
||||
names.extend(self.names(&ids));
|
||||
let selected = self
|
||||
@@ -711,7 +724,10 @@ impl App {
|
||||
let ids: Vec<_> = self
|
||||
.project
|
||||
.documents()
|
||||
.filter(|(_, d)| command == Code || matches!(d.content(), Content::Form(_)))
|
||||
.filter(|(_, d)| {
|
||||
!d.is_library()
|
||||
&& (command == Code || matches!(d.content(), Content::Form(_)))
|
||||
})
|
||||
.map(|(id, _)| id)
|
||||
.collect();
|
||||
let selected = id
|
||||
@@ -880,12 +896,22 @@ impl App {
|
||||
} else {
|
||||
Artifact::Library
|
||||
};
|
||||
let systems = vec!["linux".into(), "macos".into(), "windows".into()];
|
||||
let mut systems: Vec<String> = tb_export::Target::ALL
|
||||
.iter()
|
||||
.map(|t| crate::export::target_parts(*t).0.into())
|
||||
.collect();
|
||||
systems.sort();
|
||||
systems.dedup();
|
||||
let selected = systems
|
||||
.iter()
|
||||
.position(|s| s == std::env::consts::OS)
|
||||
.unwrap_or(0);
|
||||
let architectures = vec!["x86_64".into(), "aarch64".into()];
|
||||
let mut architectures: Vec<String> = tb_export::Target::ALL
|
||||
.iter()
|
||||
.map(|t| crate::export::target_parts(*t).1.into())
|
||||
.collect();
|
||||
architectures.sort();
|
||||
architectures.dedup();
|
||||
let arch = architectures
|
||||
.iter()
|
||||
.position(|s| s == std::env::consts::ARCH)
|
||||
@@ -906,12 +932,15 @@ impl App {
|
||||
self.output_default(if artifact == Artifact::Executable {
|
||||
"program"
|
||||
} else {
|
||||
"library"
|
||||
"library.tbl"
|
||||
}),
|
||||
),
|
||||
Field::toggle("Bestehendes Ziel überschreiben", false),
|
||||
],
|
||||
);
|
||||
if artifact == Artifact::Library {
|
||||
self.dialog.as_mut().unwrap().fields.drain(1..3);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -968,6 +997,13 @@ impl App {
|
||||
}
|
||||
}
|
||||
pub(crate) fn show_document(&mut self, id: DocumentId, form: bool) -> Result<()> {
|
||||
if self.project.document(id)?.is_library() {
|
||||
self.message = format!(
|
||||
"TBL-Bibliothek: {} · Binärabhängigkeit, kein editierbarer Quelltext",
|
||||
self.project.document(id)?.source_path().display()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
if form {
|
||||
ensure!(
|
||||
matches!(self.project.document(id)?.content(), Content::Form(_)),
|
||||
@@ -1078,7 +1114,7 @@ impl App {
|
||||
self.selected_member = 0;
|
||||
self.mode = Mode::Environment;
|
||||
self.properties = false;
|
||||
if let Some(id) = self.project.members().first().copied() {
|
||||
if let Some(id) = self.project.source_members().first().copied() {
|
||||
self.show_document(id, false)?;
|
||||
}
|
||||
let active = self.active;
|
||||
@@ -1198,7 +1234,9 @@ impl App {
|
||||
let ids: Vec<_> = self
|
||||
.project
|
||||
.documents()
|
||||
.filter(|(_, d)| !form || matches!(d.content(), Content::Form(_)))
|
||||
.filter(|(_, d)| {
|
||||
!d.is_library() && (!form || matches!(d.content(), Content::Form(_)))
|
||||
})
|
||||
.map(|(id, _)| id)
|
||||
.collect();
|
||||
let id = *ids
|
||||
@@ -1215,7 +1253,7 @@ impl App {
|
||||
Some(if replacement == 0 {
|
||||
StartupRemoval::Default
|
||||
} else {
|
||||
StartupRemoval::Replace(ids[replacement - 1])
|
||||
StartupRemoval::Replace(self.project.source_members()[replacement - 1])
|
||||
}),
|
||||
)?;
|
||||
self.selected_member = self
|
||||
@@ -1223,7 +1261,7 @@ impl App {
|
||||
.min(self.project.members().len().saturating_sub(1));
|
||||
}
|
||||
DialogKind::Startup => {
|
||||
let ids = self.project.members();
|
||||
let ids = self.project.source_members();
|
||||
let choice = d.fields[0].index();
|
||||
self.project.set_startup(if choice == 0 {
|
||||
None
|
||||
@@ -1234,7 +1272,11 @@ impl App {
|
||||
DialogKind::Dirty(after) => match d.fields[0].index() {
|
||||
1 => self.save_dialog(
|
||||
true,
|
||||
self.project.documents().map(|(id, _)| id).collect(),
|
||||
self.project
|
||||
.documents()
|
||||
.filter(|(_, d)| !d.is_library())
|
||||
.map(|(id, _)| id)
|
||||
.collect(),
|
||||
after,
|
||||
)?,
|
||||
2 => self.finish_change(after)?,
|
||||
@@ -1352,25 +1394,83 @@ impl App {
|
||||
self.message = format!("Optionen gespeichert: {}", self.config_path.display());
|
||||
}
|
||||
DialogKind::Export(artifact) => {
|
||||
let library = artifact == Artifact::Library;
|
||||
let output = if library { 1 } else { 3 };
|
||||
let request = ExportRequest::validate(
|
||||
&self.project,
|
||||
artifact,
|
||||
&d.fields[1].string(),
|
||||
&d.fields[2].string(),
|
||||
&if library {
|
||||
String::new()
|
||||
} else {
|
||||
d.fields[1].string()
|
||||
},
|
||||
&if library {
|
||||
String::new()
|
||||
} else {
|
||||
d.fields[2].string()
|
||||
},
|
||||
&Destination {
|
||||
path: d.fields[3].string().into(),
|
||||
overwrite: d.fields[4].flag(),
|
||||
path: d.fields[output].string().into(),
|
||||
overwrite: d.fields[output + 1].flag(),
|
||||
},
|
||||
)?;
|
||||
d.request = Some(request);
|
||||
self.export_job = None;
|
||||
d.request = Some(request.clone());
|
||||
if self.export_backend {
|
||||
match crate::export::Job::start(request, &self.project, &self.runtime_dir) {
|
||||
Ok(job) => {
|
||||
self.export_job = Some(job);
|
||||
d.export_status = ExportStatus::Running;
|
||||
d.error.clear();
|
||||
}
|
||||
Err(e) => {
|
||||
d.export_status = ExportStatus::Failed(format!("{e:#}"));
|
||||
d.error.clear();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
d.export_status = ExportStatus::Ready;
|
||||
d.error = crate::export::UNAVAILABLE.into();
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
/// Die spätere Anbindung und Oberflächentests liefern denselben konkreten Auftrag zurück.
|
||||
/// Der UI-Thread veröffentlicht ausschließlich den weiterhin gültigen Auftrag.
|
||||
pub fn tick_export(&mut self) {
|
||||
let Some(job) = &self.export_job else {
|
||||
return;
|
||||
};
|
||||
let request = job.request.clone();
|
||||
let same_dialog = self.dialog.as_ref().and_then(|d| d.request.as_ref()) == Some(&request);
|
||||
if !same_dialog || request.project != crate::export::ProjectStamp::capture(&self.project) {
|
||||
self.export_job = None;
|
||||
if same_dialog {
|
||||
if let Some(d) = &mut self.dialog {
|
||||
d.export_status = ExportStatus::Failed(
|
||||
"Projekt wurde seit dem Exportauftrag geändert".into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(result) = job.take_result() {
|
||||
let status = match result.and_then(|prepared| {
|
||||
ensure!(
|
||||
std::fs::read(&request.path).ok() == request.previous_output,
|
||||
"Ziel wurde seit dem Exportauftrag verändert"
|
||||
);
|
||||
prepared.publish(&|| false)
|
||||
}) {
|
||||
Ok(()) => ExportStatus::Success,
|
||||
Err(e) => ExportStatus::Failed(format!("{e:#}")),
|
||||
};
|
||||
self.export_job = None;
|
||||
let _ = self.export_result(&request, status);
|
||||
}
|
||||
}
|
||||
/// Oberflächentests und die geprüfte Veröffentlichung melden denselben Auftrag zurück.
|
||||
pub fn export_result(&mut self, request: &ExportRequest, status: ExportStatus) -> Result<()> {
|
||||
let d = self
|
||||
.dialog
|
||||
@@ -1639,16 +1739,19 @@ impl App {
|
||||
}
|
||||
}
|
||||
fn cancel_dialog(&mut self) {
|
||||
self.export_job = None;
|
||||
if let Some(d) = self.dialog.take() {
|
||||
if let DialogKind::Browse { return_to, .. } = d.kind {
|
||||
self.dialog = Some(*return_to);
|
||||
return;
|
||||
}
|
||||
if let Some(request) = d.request {
|
||||
if d.export_status != ExportStatus::Success {
|
||||
self.last_export = Some((request, ExportStatus::Cancelled));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn program_focus(&self) -> bool {
|
||||
matches!(self.execution, Execution::Running | Execution::Waiting)
|
||||
&& (self.session.fullscreen
|
||||
@@ -1958,7 +2061,9 @@ impl App {
|
||||
| DialogKind::SaveText
|
||||
| DialogKind::Print => index == 0,
|
||||
DialogKind::Save { .. } => index % 2 == 0,
|
||||
DialogKind::Export(_) => index == 3,
|
||||
DialogKind::Export(artifact) => {
|
||||
index == if artifact == Artifact::Library { 1 } else { 3 }
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !allowed || index >= dialog.fields.len() {
|
||||
|
||||
@@ -427,6 +427,15 @@ impl App {
|
||||
.is_ok_and(|doc| module_name(doc).eq_ignore_ascii_case(&owner_name))
|
||||
});
|
||||
self.session.fullscreen = false;
|
||||
if self
|
||||
.project
|
||||
.find_document(std::path::Path::new(&file))
|
||||
.is_none()
|
||||
&& !std::path::Path::new(&file).is_file()
|
||||
{
|
||||
self.message = format!("Bibliotheksquelle nicht verfügbar: {file}:{} · Fortsetzen oder Prozedurschritt verwenden", location.line);
|
||||
return Ok(());
|
||||
}
|
||||
self.goto_diagnostic(&Diagnostic {
|
||||
file: Some(file.clone()),
|
||||
pos: SourcePos {
|
||||
|
||||
@@ -102,6 +102,9 @@ fn remap_breakpoints(
|
||||
.collect()
|
||||
}
|
||||
impl Document {
|
||||
pub fn is_library(&self) -> bool {
|
||||
has_extension(&self.source_path, "tbl")
|
||||
}
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.path.as_deref()
|
||||
}
|
||||
@@ -247,6 +250,12 @@ impl Project {
|
||||
.filter_map(|p| self.find_document(p))
|
||||
.collect()
|
||||
}
|
||||
pub fn source_members(&self) -> Vec<DocumentId> {
|
||||
self.members()
|
||||
.into_iter()
|
||||
.filter(|id| !self.documents[id].is_library())
|
||||
.collect()
|
||||
}
|
||||
pub fn startup(&self) -> Option<DocumentId> {
|
||||
self.manifest
|
||||
.startup
|
||||
@@ -260,7 +269,7 @@ impl Project {
|
||||
pub fn loader(&self) -> Result<SourceLoader> {
|
||||
let mut loader = SourceLoader::default();
|
||||
loader.include_paths = self.include_paths.clone();
|
||||
for doc in self.documents.values() {
|
||||
for doc in self.documents.values().filter(|d| !d.is_library()) {
|
||||
loaded(loader.insert(&doc.source_path, doc.content.clone()))?;
|
||||
}
|
||||
Ok(loader)
|
||||
@@ -279,7 +288,18 @@ impl Project {
|
||||
return Ok(id);
|
||||
}
|
||||
ensure!(!has_extension(&path, "mak"), "MAK über Open Project öffnen");
|
||||
let read = loaded(read_document(&path))?;
|
||||
let read = if has_extension(&path, "tbl") {
|
||||
let bytes = fs::read(&path)?;
|
||||
tb_vm::library::Library::from_tbl(&bytes).map_err(|e| anyhow!(e))?;
|
||||
tb_vm::project_io::ReadDocument {
|
||||
content: Content::Text(String::new()),
|
||||
bytes,
|
||||
binary: false,
|
||||
warnings: vec![],
|
||||
}
|
||||
} else {
|
||||
loaded(read_document(&path))?
|
||||
};
|
||||
let id = DocumentId(next_id());
|
||||
self.documents.insert(
|
||||
id,
|
||||
@@ -301,15 +321,17 @@ impl Project {
|
||||
}
|
||||
pub fn add_file(&mut self, path: &Path) -> Result<DocumentId> {
|
||||
ensure!(
|
||||
has_extension(path, "bas") || has_extension(path, "frm"),
|
||||
"Projektmitglied muss BAS oder FRM sein"
|
||||
has_extension(path, "bas") || has_extension(path, "frm") || has_extension(path, "tbl"),
|
||||
"Projektmitglied muss BAS, FRM oder TBL sein"
|
||||
);
|
||||
// Auch die hinzugefügte Include-Kette muss vor jeder Änderung gültig sein.
|
||||
let loader = self.loader()?;
|
||||
let path = identity(&loaded(
|
||||
loader.resolve(&self.base, &path.to_string_lossy()),
|
||||
)?)?;
|
||||
if !has_extension(&path, "tbl") {
|
||||
loaded(loader.load(&path))?;
|
||||
}
|
||||
let id = self.open_document(&path)?;
|
||||
if !self.manifest.members().any(|p| p == &path) {
|
||||
self.manifest.lines.push(ProjectLine::File(path));
|
||||
@@ -403,6 +425,7 @@ impl Project {
|
||||
.map(|id| self.document(id).map(|d| d.source_path.clone()))
|
||||
.transpose()?;
|
||||
if let Some(path) = &path {
|
||||
ensure!(!has_extension(path, "tbl"), "TBL ist keine Startdatei");
|
||||
ensure!(
|
||||
self.manifest.members().any(|p| p == path),
|
||||
"Startdatei ist kein Projektmitglied"
|
||||
@@ -436,7 +459,10 @@ impl Project {
|
||||
Ok(true)
|
||||
}
|
||||
pub fn open_view(&mut self, document: DocumentId) -> Result<ViewId> {
|
||||
self.document(document)?;
|
||||
ensure!(
|
||||
!self.document(document)?.is_library(),
|
||||
"TBL ist eine Binärbibliothek, kein editierbarer Quelltext"
|
||||
);
|
||||
let id = ViewId(next_id());
|
||||
self.views.insert(
|
||||
id,
|
||||
@@ -546,6 +572,7 @@ impl Project {
|
||||
}
|
||||
}
|
||||
fn commit_edit(&mut self, id: DocumentId, content: Content) -> Result<()> {
|
||||
ensure!(!self.document(id)?.is_library(), "TBL ist schreibgeschützt");
|
||||
self.design_ids(id)?;
|
||||
let old = self.document(id)?;
|
||||
if old.content == content {
|
||||
@@ -740,6 +767,7 @@ impl Project {
|
||||
target: &Destination,
|
||||
) -> Result<()> {
|
||||
let doc = self.document(id)?;
|
||||
ensure!(!doc.is_library(), "TBL ist kein Textdokument");
|
||||
let text = match range {
|
||||
Some(r) => doc
|
||||
.code()
|
||||
@@ -806,6 +834,10 @@ impl Project {
|
||||
Ok(())
|
||||
}
|
||||
fn file_target(&self, id: DocumentId, target: Option<&Destination>) -> Result<(PathBuf, bool)> {
|
||||
ensure!(
|
||||
!self.document(id)?.is_library(),
|
||||
"TBL wird als Binärabhängigkeit referenziert und nicht gespeichert"
|
||||
);
|
||||
let doc = self.document(id)?;
|
||||
let path = match target {
|
||||
Some(t) => self.target(&t.path)?,
|
||||
@@ -832,8 +864,10 @@ impl Project {
|
||||
ensure!(has_extension(&path, "bas"), "Modulziel muss .bas sein")
|
||||
}
|
||||
Content::Text(_) => ensure!(
|
||||
!has_extension(&path, "frm") && !has_extension(&path, "mak"),
|
||||
"Textziel darf kein FRM/MAK sein"
|
||||
!has_extension(&path, "frm")
|
||||
&& !has_extension(&path, "mak")
|
||||
&& !has_extension(&path, "tbl"),
|
||||
"Textziel darf kein FRM/MAK/TBL sein"
|
||||
),
|
||||
}
|
||||
Ok((path, target.is_some_and(|t| t.overwrite)))
|
||||
|
||||
@@ -118,7 +118,7 @@ impl App {
|
||||
let target = if project {
|
||||
self.project.path().map(PathBuf::from).or_else(|| {
|
||||
self.project
|
||||
.members()
|
||||
.source_members()
|
||||
.first()
|
||||
.and_then(|id| self.project.document(*id).ok())
|
||||
.map(|d| d.source_path().to_path_buf())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Konkreter Vertrag für die spätere native Erzeugung, ohne Phase-5-Dateiausgabe.
|
||||
//! Revisionsgebundene Exportaufträge und abwerfbare Hintergrundresultate.
|
||||
use crate::documents::{Destination, DocumentId, Project};
|
||||
use anyhow::{ensure, Result};
|
||||
use std::path::PathBuf;
|
||||
@@ -14,7 +14,7 @@ impl Artifact {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Executable => "Natives Standalone-Executable",
|
||||
Self::Library => "Native Systembibliothek",
|
||||
Self::Library => "Portable P-Code-Bibliothek (.tbl)",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ impl Artifact {
|
||||
pub struct ProjectStamp {
|
||||
pub path: Option<PathBuf>,
|
||||
pub manifest: Manifest,
|
||||
pub inputs: Vec<(PathBuf, Option<Vec<u8>>)>,
|
||||
pub include_paths: Vec<PathBuf>,
|
||||
pub revisions: Vec<(DocumentId, u64)>,
|
||||
}
|
||||
impl ProjectStamp {
|
||||
@@ -29,6 +31,20 @@ impl ProjectStamp {
|
||||
Self {
|
||||
path: project.path().map(PathBuf::from),
|
||||
manifest: project.manifest().clone(),
|
||||
include_paths: project.include_paths.clone(),
|
||||
inputs: project
|
||||
.sources()
|
||||
.map(|s| {
|
||||
s.protected_inputs()
|
||||
.into_iter()
|
||||
.filter(|p| has_extension(p, "tbl") || project.find_document(p).is_none())
|
||||
.map(|p| {
|
||||
let b = std::fs::read(&p).ok();
|
||||
(p, b)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
revisions: project
|
||||
.documents()
|
||||
.map(|(id, d)| (id, d.revision()))
|
||||
@@ -38,12 +54,14 @@ impl ProjectStamp {
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ExportRequest {
|
||||
pub generation: u64,
|
||||
pub project: ProjectStamp,
|
||||
pub artifact: Artifact,
|
||||
pub system: String,
|
||||
pub architecture: String,
|
||||
pub path: PathBuf,
|
||||
pub overwrite: bool,
|
||||
pub previous_output: Option<Vec<u8>>,
|
||||
}
|
||||
impl ExportRequest {
|
||||
pub fn validate(
|
||||
@@ -53,24 +71,25 @@ impl ExportRequest {
|
||||
architecture: &str,
|
||||
destination: &Destination,
|
||||
) -> Result<Self> {
|
||||
ensure!(
|
||||
["linux", "macos", "windows"].contains(&system),
|
||||
"Unbekanntes Zielsystem"
|
||||
);
|
||||
ensure!(
|
||||
["x86_64", "aarch64"].contains(&architecture),
|
||||
"Unbekannte Zielarchitektur"
|
||||
);
|
||||
if artifact == Artifact::Executable {
|
||||
target(system, architecture)?;
|
||||
}
|
||||
ensure!(
|
||||
!has_extension(&destination.path, "tbc"),
|
||||
"TBC ist kein natives Exportartefakt"
|
||||
);
|
||||
let path = project.validate_export_target(destination)?;
|
||||
if artifact == Artifact::Library {
|
||||
ensure!(has_extension(&path, "tbl"), "Library-Ausgabe benötigt .tbl");
|
||||
}
|
||||
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
Ok(Self {
|
||||
generation: NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||
project: ProjectStamp::capture(project),
|
||||
artifact,
|
||||
system: system.into(),
|
||||
architecture: architecture.into(),
|
||||
previous_output: std::fs::read(&path).ok(),
|
||||
path,
|
||||
overwrite: destination.overwrite,
|
||||
})
|
||||
@@ -95,3 +114,333 @@ impl ExportStatus {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Die unterstützten Kombinationen werden ausschließlich aus dem Exportkatalog gewählt.
|
||||
pub fn target(system: &str, architecture: &str) -> Result<tb_export::Target> {
|
||||
tb_export::Target::ALL
|
||||
.into_iter()
|
||||
.find(|t| target_parts(*t) == (system, architecture))
|
||||
.ok_or_else(|| anyhow::anyhow!("Unzulässiges EXE-Ziel: {system}/{architecture}"))
|
||||
}
|
||||
pub fn target_parts(target: tb_export::Target) -> (&'static str, &'static str) {
|
||||
let triple = target.triple();
|
||||
(
|
||||
if triple.contains("windows") {
|
||||
"windows"
|
||||
} else if triple.contains("apple") {
|
||||
"macos"
|
||||
} else {
|
||||
"linux"
|
||||
},
|
||||
triple.split('-').next().unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub struct Job {
|
||||
pub request: ExportRequest,
|
||||
thread: Option<std::thread::JoinHandle<()>>,
|
||||
cancelled: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
result: std::sync::mpsc::Receiver<Result<tb_export::PreparedPublication>>,
|
||||
}
|
||||
impl Drop for Job {
|
||||
fn drop(&mut self) {
|
||||
self.cancelled
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
// Auch beim Beenden der IDE müssen kontrollierte Tools und Staging enden.
|
||||
if let Some(thread) = self.thread.take() {
|
||||
let _ = thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Job {
|
||||
pub fn start(
|
||||
request: ExportRequest,
|
||||
project: &Project,
|
||||
runtime_dir: &std::path::Path,
|
||||
) -> Result<Self> {
|
||||
let sources = project.sources()?;
|
||||
let mut compiler = tb_vm::project::ProjectCompiler::default();
|
||||
compiler.debug_symbols = true;
|
||||
let diagnostics = |errors: Vec<tb_frontend::Diagnostic>| {
|
||||
anyhow::anyhow!(errors
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"))
|
||||
};
|
||||
let bytes = match request.artifact {
|
||||
Artifact::Library => sources
|
||||
.compile_library(&mut compiler)
|
||||
.map_err(diagnostics)?
|
||||
.to_tbl()
|
||||
.map_err(|e| anyhow::anyhow!(e))?,
|
||||
Artifact::Executable => sources
|
||||
.compile(&mut compiler, "IDE")
|
||||
.map_err(diagnostics)?
|
||||
.to_tbc(),
|
||||
};
|
||||
ensure!(
|
||||
request.project == ProjectStamp::capture(project),
|
||||
"Projekt während der Übersetzung geändert"
|
||||
);
|
||||
let mut protected = sources.protected_inputs();
|
||||
protected.extend(
|
||||
project
|
||||
.documents()
|
||||
.map(|(_, d)| d.source_path().to_path_buf()),
|
||||
);
|
||||
if let Some(path) = project.path() {
|
||||
protected.push(path.into());
|
||||
}
|
||||
let runtime_dir = runtime_dir.to_path_buf();
|
||||
let cancelled = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let flag = cancelled.clone();
|
||||
let (sender, result) = std::sync::mpsc::channel();
|
||||
let work = request.clone();
|
||||
let thread = std::thread::Builder::new()
|
||||
.name("tb-export".into())
|
||||
.spawn(move || {
|
||||
let cancelled = || flag.load(std::sync::atomic::Ordering::Acquire);
|
||||
let prepare = || -> Result<tb_export::PreparedPublication> {
|
||||
if work.artifact == Artifact::Library {
|
||||
tb_export::prepare_publication(
|
||||
&work.path,
|
||||
work.overwrite,
|
||||
&protected,
|
||||
&cancelled,
|
||||
|p| {
|
||||
std::fs::write(p, &bytes)?;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let target = target(&work.system, &work.architecture)?;
|
||||
let template = runtime_dir
|
||||
.join(target.triple())
|
||||
.join(target.runtime_name());
|
||||
let module = tb_vm::bytecode::CompiledModule::from_tbc(&bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
tb_export::prepare_export(
|
||||
tb_export::Export {
|
||||
module: &module,
|
||||
target,
|
||||
template: &template,
|
||||
output: &work.path,
|
||||
overwrite: work.overwrite,
|
||||
protected: &protected,
|
||||
},
|
||||
&cancelled,
|
||||
)
|
||||
}
|
||||
};
|
||||
// Ein geschlossener Empfänger verwirft das Staging-Ergebnis per Drop.
|
||||
let _ = sender.send(prepare());
|
||||
})?;
|
||||
Ok(Self {
|
||||
thread: Some(thread),
|
||||
request,
|
||||
cancelled,
|
||||
result,
|
||||
})
|
||||
}
|
||||
pub fn take_result(&self) -> Option<Result<tb_export::PreparedPublication>> {
|
||||
match self.result.try_recv() {
|
||||
Ok(result) => Some(result),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => None,
|
||||
Err(_) => Some(Err(anyhow::anyhow!("Exportworker beendet ohne Ergebnis"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{app::App, commands::Command};
|
||||
use std::{
|
||||
fs,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
mpsc, Arc,
|
||||
},
|
||||
};
|
||||
|
||||
struct Dir(PathBuf);
|
||||
impl Dir {
|
||||
fn new() -> Self {
|
||||
static N: AtomicU64 = AtomicU64::new(0);
|
||||
let p = std::env::temp_dir().join(format!(
|
||||
"tb-export-ui-{}-{}",
|
||||
std::process::id(),
|
||||
N.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
Self(p.canonicalize().unwrap())
|
||||
}
|
||||
}
|
||||
impl Drop for Dir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
fn delayed(
|
||||
app: &mut App,
|
||||
output: &std::path::Path,
|
||||
) -> (
|
||||
mpsc::Sender<Result<tb_export::PreparedPublication>>,
|
||||
Arc<AtomicBool>,
|
||||
) {
|
||||
app.execute(Command::MakeLibrary);
|
||||
let request = ExportRequest::validate(
|
||||
&app.project,
|
||||
Artifact::Library,
|
||||
"",
|
||||
"",
|
||||
&Destination {
|
||||
path: output.into(),
|
||||
overwrite: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let d = app.dialog.as_mut().unwrap();
|
||||
d.request = Some(request.clone());
|
||||
d.export_status = ExportStatus::Running;
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let cancelled = Arc::new(AtomicBool::new(false));
|
||||
app.export_job = Some(Job {
|
||||
thread: None,
|
||||
request,
|
||||
cancelled: cancelled.clone(),
|
||||
result: rx,
|
||||
});
|
||||
(tx, cancelled)
|
||||
}
|
||||
fn prepared(output: &std::path::Path) -> tb_export::PreparedPublication {
|
||||
tb_export::prepare_publication(output, true, &[], &|| false, |p| {
|
||||
fs::write(p, b"new")?;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
fn no_staging(dir: &std::path::Path) {
|
||||
assert!(fs::read_dir(dir).unwrap().all(|p| !p
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.starts_with(".tb-export-")));
|
||||
}
|
||||
#[test]
|
||||
fn delayed_result_is_published_only_after_ui_validation_and_success_stays_success() {
|
||||
let dir = Dir::new();
|
||||
let mut app = App::new(&dir.0, dir.0.join("options"), (100, 30)).unwrap();
|
||||
let output = dir.0.join("out.tbl");
|
||||
fs::write(&output, b"old").unwrap();
|
||||
let (tx, _) = delayed(&mut app, &output);
|
||||
let staged = prepared(&output);
|
||||
app.tick_export();
|
||||
app.handle(crossterm::event::Event::Resize(110, 35));
|
||||
assert_eq!(app.size, (110, 35));
|
||||
assert_eq!(fs::read(&output).unwrap(), b"old");
|
||||
tx.send(Ok(staged)).ok().unwrap();
|
||||
app.tick_export();
|
||||
assert_eq!(fs::read(&output).unwrap(), b"new");
|
||||
assert_eq!(app.last_export.as_ref().unwrap().1, ExportStatus::Success);
|
||||
app.handle(crossterm::event::Event::Key(
|
||||
crossterm::event::KeyEvent::new(
|
||||
crossterm::event::KeyCode::Esc,
|
||||
crossterm::event::KeyModifiers::NONE,
|
||||
),
|
||||
));
|
||||
assert_eq!(app.last_export.as_ref().unwrap().1, ExportStatus::Success);
|
||||
no_staging(&dir.0);
|
||||
}
|
||||
#[test]
|
||||
fn cancelled_superseded_changed_and_foreign_jobs_never_publish() {
|
||||
for mode in 0..5 {
|
||||
let dir = Dir::new();
|
||||
let mut app = App::new(&dir.0, dir.0.join("options"), (100, 30)).unwrap();
|
||||
let output = dir.0.join("out.tbl");
|
||||
fs::write(&output, b"old").unwrap();
|
||||
let (tx, flag) = delayed(&mut app, &output);
|
||||
let staged = prepared(&output);
|
||||
match mode {
|
||||
0 => app.handle(crossterm::event::Event::Key(
|
||||
crossterm::event::KeyEvent::new(
|
||||
crossterm::event::KeyCode::Esc,
|
||||
crossterm::event::KeyModifiers::NONE,
|
||||
),
|
||||
)),
|
||||
1 => {
|
||||
let id = app.project.members()[0];
|
||||
app.project.replace_text(id, 0..0, "PRINT 2\n").unwrap();
|
||||
}
|
||||
2 => {
|
||||
app.project = Project::new(&dir.0).unwrap();
|
||||
}
|
||||
3 => {
|
||||
let old = app.export_job.as_ref().unwrap().request.generation;
|
||||
let (_new, _) = delayed(&mut app, &output);
|
||||
assert_ne!(old, app.export_job.as_ref().unwrap().request.generation);
|
||||
}
|
||||
_ => {
|
||||
fs::write(&output, b"external").unwrap();
|
||||
}
|
||||
}
|
||||
let _ = tx.send(Ok(staged));
|
||||
app.tick_export();
|
||||
assert!(flag.load(Ordering::Acquire));
|
||||
assert_eq!(
|
||||
fs::read(&output).unwrap(),
|
||||
if mode == 4 {
|
||||
b"external".as_slice()
|
||||
} else {
|
||||
b"old".as_slice()
|
||||
}
|
||||
);
|
||||
assert!(!app
|
||||
.last_export
|
||||
.as_ref()
|
||||
.is_some_and(|(_, s)| *s == ExportStatus::Success));
|
||||
no_staging(&dir.0);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn replaced_library_invalidates_pending_export_even_with_same_length() {
|
||||
let dir = Dir::new();
|
||||
let source = dir.0.join("lib.bas");
|
||||
fs::write(&source, "SUB Answer\nPRINT 1\nEND SUB\n").unwrap();
|
||||
let mut compiler = tb_vm::project::ProjectCompiler::default();
|
||||
let build = |compiler: &mut tb_vm::project::ProjectCompiler| {
|
||||
tb_vm::project_io::SourceLoader::default()
|
||||
.load(&source)
|
||||
.unwrap()
|
||||
.compile_library(compiler)
|
||||
.unwrap()
|
||||
.to_tbl()
|
||||
.unwrap()
|
||||
};
|
||||
let a = build(&mut compiler);
|
||||
fs::write(&source, "SUB Answer\nPRINT 2\nEND SUB\n").unwrap();
|
||||
let b = build(&mut compiler);
|
||||
assert_eq!(a.len(), b.len());
|
||||
let lib = dir.0.join("lib.tbl");
|
||||
fs::write(&lib, &a).unwrap();
|
||||
fs::write(dir.0.join("main.bas"), "CALL Answer\nEND\n").unwrap();
|
||||
fs::write(dir.0.join("main.mak"), "main.bas\nlib.tbl\n").unwrap();
|
||||
let mut app = App::new(&dir.0, dir.0.join("options"), (100, 30)).unwrap();
|
||||
app.load_initial_project(dir.0.join("main.mak")).unwrap();
|
||||
let before = app.compile_current().unwrap().to_tbc();
|
||||
assert!(app.current_compilation().is_some());
|
||||
let out = dir.0.join("out.tbl");
|
||||
fs::write(&out, b"old").unwrap();
|
||||
let (tx, flag) = delayed(&mut app, &out);
|
||||
let staged = prepared(&out);
|
||||
fs::write(&lib, &b).unwrap();
|
||||
assert!(app.current_compilation().is_none());
|
||||
let _ = tx.send(Ok(staged));
|
||||
app.tick_export();
|
||||
assert!(flag.load(Ordering::Acquire));
|
||||
assert_eq!(fs::read(&out).unwrap(), b"old");
|
||||
assert_ne!(before, app.compile_current().unwrap().to_tbc());
|
||||
no_staging(&dir.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,14 @@ const DOCUMENTS: &[(&str, &str)] = &[
|
||||
"docs/tbvm-design.md",
|
||||
include_str!("../../../docs/tbvm-design.md"),
|
||||
),
|
||||
(
|
||||
"docs/pcode-bibliotheken.md",
|
||||
include_str!("../../../docs/pcode-bibliotheken.md"),
|
||||
),
|
||||
(
|
||||
"docs/native-executables.md",
|
||||
include_str!("../../../docs/native-executables.md"),
|
||||
),
|
||||
("PLAN.md", include_str!("../../../PLAN.md")),
|
||||
];
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -70,6 +70,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
app.handle(event::read()?);
|
||||
}
|
||||
app.tick_export();
|
||||
app.tick_execution(start.elapsed().as_millis() as u64);
|
||||
let timeout = if app.execution == Execution::Running {
|
||||
std::time::Duration::ZERO
|
||||
|
||||
@@ -661,7 +661,15 @@ impl App {
|
||||
put(
|
||||
f,
|
||||
submit,
|
||||
if export { "[Prüfen]" } else { "[OK / Enter]" },
|
||||
if export {
|
||||
if self.export_backend {
|
||||
"[Erzeugen]"
|
||||
} else {
|
||||
"[Prüfen]"
|
||||
}
|
||||
} else {
|
||||
"[OK / Enter]"
|
||||
},
|
||||
if d.focus == d.fields.len() {
|
||||
Style::default().fg(Color::White).bg(Color::Black)
|
||||
} else {
|
||||
@@ -671,7 +679,7 @@ impl App {
|
||||
put(f, cancel, "[Abbrechen]", st);
|
||||
self.hits.push((submit, Hit::DialogSubmit));
|
||||
self.hits.push((cancel, Hit::DialogCancel));
|
||||
if export {
|
||||
if export && !self.export_backend {
|
||||
put(
|
||||
f,
|
||||
Rect::new(rect.x + 36, rect.bottom() - 3, width - 38, 1),
|
||||
@@ -689,7 +697,11 @@ impl App {
|
||||
d.error
|
||||
);
|
||||
} else if status.is_empty() {
|
||||
status = crate::export::UNAVAILABLE.into();
|
||||
status = if self.export_backend {
|
||||
"Bereit zum Erzeugen".into()
|
||||
} else {
|
||||
crate::export::UNAVAILABLE.into()
|
||||
};
|
||||
}
|
||||
}
|
||||
f.render_widget(
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::{
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
use tb_ide::{
|
||||
app::{App, DialogKind, Execution, Mode, WindowKind, WindowState},
|
||||
app::{App, DialogKind, Execution, Hit, Mode, WindowKind, WindowState},
|
||||
commands::{self, Command},
|
||||
export::{ExportStatus, ProjectStamp},
|
||||
options::Options,
|
||||
@@ -347,23 +347,33 @@ fn status_and_project_buttons_use_the_keyboard_command_path() {
|
||||
fn export_dialogs_validate_and_render_all_results_without_creating_files() {
|
||||
let t = Temp::new();
|
||||
let mut app = t.app();
|
||||
app.export_backend = false;
|
||||
let stamp = ProjectStamp::capture(&app.project);
|
||||
for command in [Command::MakeExe, Command::MakeLibrary] {
|
||||
menu(&mut app, command);
|
||||
while app.dialog.as_ref().unwrap().focus != 1 {
|
||||
plain(&mut app, K::Tab);
|
||||
}
|
||||
plain(&mut app, K::Right);
|
||||
let system = app.dialog.as_ref().unwrap().fields[1].string();
|
||||
plain(&mut app, K::Tab);
|
||||
plain(&mut app, K::Right);
|
||||
let architecture = app.dialog.as_ref().unwrap().fields[2].string();
|
||||
let output = if command == Command::MakeLibrary {
|
||||
1
|
||||
} else {
|
||||
3
|
||||
};
|
||||
let (system, architecture) = if command == Command::MakeExe {
|
||||
(
|
||||
app.dialog.as_ref().unwrap().fields[1].string(),
|
||||
app.dialog.as_ref().unwrap().fields[2].string(),
|
||||
)
|
||||
} else {
|
||||
(String::new(), String::new())
|
||||
};
|
||||
let (text, _) = draw(&mut app);
|
||||
assert!(text.contains("Erzeugen: Phase 6"));
|
||||
field(&mut app, 3, "");
|
||||
field(&mut app, output, "");
|
||||
plain(&mut app, K::Enter);
|
||||
assert!(app.dialog.as_ref().unwrap().error.contains("Ausgabepfad"));
|
||||
field(&mut app, 3, &t.0.join("notthere/out").display().to_string());
|
||||
field(
|
||||
&mut app,
|
||||
output,
|
||||
&t.0.join("notthere/out").display().to_string(),
|
||||
);
|
||||
plain(&mut app, K::Enter);
|
||||
assert!(app
|
||||
.dialog
|
||||
@@ -371,12 +381,12 @@ fn export_dialogs_validate_and_render_all_results_without_creating_files() {
|
||||
.unwrap()
|
||||
.error
|
||||
.contains("Zielverzeichnis"));
|
||||
field(&mut app, 3, &t.0.join("bad.tbc").display().to_string());
|
||||
field(&mut app, output, &t.0.join("bad.tbc").display().to_string());
|
||||
plain(&mut app, K::Enter);
|
||||
assert!(app.dialog.as_ref().unwrap().error.contains("TBC"));
|
||||
let existing = t.0.join("existing");
|
||||
let existing = t.0.join("existing.tbl");
|
||||
fs::write(&existing, b"keep").unwrap();
|
||||
field(&mut app, 3, &existing.display().to_string());
|
||||
field(&mut app, output, &existing.display().to_string());
|
||||
plain(&mut app, K::Enter);
|
||||
assert!(app
|
||||
.dialog
|
||||
@@ -389,8 +399,8 @@ fn export_dialogs_validate_and_render_all_results_without_creating_files() {
|
||||
plain(&mut app, K::Enter);
|
||||
assert_eq!(fs::read(&existing).unwrap(), b"keep");
|
||||
assert!(app.dialog.as_ref().unwrap().request.is_some());
|
||||
let target = t.0.join("result");
|
||||
field(&mut app, 3, &target.display().to_string());
|
||||
let target = t.0.join("result.tbl");
|
||||
field(&mut app, output, &target.display().to_string());
|
||||
plain(&mut app, K::Enter);
|
||||
let request = app.dialog.as_ref().unwrap().request.clone().unwrap();
|
||||
assert_eq!(request.system, system);
|
||||
@@ -702,3 +712,215 @@ fn options_apply_persist_across_bases_and_preserve_unsaved_values_on_error() {
|
||||
assert_eq!(defaults, Options::default());
|
||||
assert_eq!(errors.len(), 4);
|
||||
}
|
||||
|
||||
fn await_export(app: &mut App) -> ExportStatus {
|
||||
let start = std::time::Instant::now();
|
||||
loop {
|
||||
app.tick_export();
|
||||
let status = app.dialog.as_ref().unwrap().export_status.clone();
|
||||
if status != ExportStatus::Running {
|
||||
return status;
|
||||
}
|
||||
assert!(start.elapsed().as_secs() < 10, "Export hängt");
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_library_export_uses_dirty_include_and_mouse_without_runtime_template() {
|
||||
let t = Temp::new();
|
||||
fs::write(
|
||||
t.0.join("lib.bas"),
|
||||
"SUB Answer\n'$INCLUDE: 'body.bi'\nEND SUB\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(t.0.join("body.bi"), "PRINT 1\n").unwrap();
|
||||
let mut app = t.app();
|
||||
app.load_initial_project(t.0.join("lib.bas")).unwrap();
|
||||
let include = app.project.find_document(&t.0.join("body.bi")).unwrap();
|
||||
app.project
|
||||
.replace_text(include, 0..8, "PRINT 42\n")
|
||||
.unwrap();
|
||||
let stamp = ProjectStamp::capture(&app.project);
|
||||
menu(&mut app, Command::MakeLibrary);
|
||||
assert_eq!(app.dialog.as_ref().unwrap().fields.len(), 3);
|
||||
let out = t.0.join("out.tbl");
|
||||
field(&mut app, 1, &out.display().to_string());
|
||||
draw(&mut app);
|
||||
let r = app
|
||||
.hits
|
||||
.iter()
|
||||
.find(|(_, h)| matches!(h, Hit::DialogSubmit))
|
||||
.unwrap()
|
||||
.0;
|
||||
app.handle(Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: r.x,
|
||||
row: r.y,
|
||||
modifiers: M::NONE,
|
||||
}));
|
||||
assert_eq!(await_export(&mut app), ExportStatus::Success);
|
||||
assert_eq!(ProjectStamp::capture(&app.project), stamp);
|
||||
assert!(app.project.document(include).unwrap().is_dirty());
|
||||
assert_eq!(
|
||||
fs::read_to_string(t.0.join("body.bi")).unwrap(),
|
||||
"PRINT 1\n"
|
||||
);
|
||||
plain(&mut app, K::Esc);
|
||||
assert_eq!(app.last_export.as_ref().unwrap().1, ExportStatus::Success);
|
||||
fs::remove_file(t.0.join("lib.bas")).unwrap();
|
||||
fs::remove_file(t.0.join("body.bi")).unwrap();
|
||||
fs::write(t.0.join("consumer.bas"), "CALL Answer\nEND\n").unwrap();
|
||||
fs::write(t.0.join("consumer.mak"), "consumer.bas\nout.tbl\n").unwrap();
|
||||
app.load_initial_project(t.0.join("consumer.mak")).unwrap();
|
||||
app.start_execution().unwrap();
|
||||
for n in 0..100 {
|
||||
app.tick_execution(n);
|
||||
if app.execution == Execution::Ended {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(tb_runtime::snapshot::text(app.session.screen()), " 42 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executable_target_validation_and_missing_template_are_honest() {
|
||||
use tb_ide::{
|
||||
documents::Destination,
|
||||
export::{Artifact, ExportRequest},
|
||||
};
|
||||
let t = Temp::new();
|
||||
let mut app = t.app();
|
||||
for (system, arch) in [
|
||||
("windows", "aarch64"),
|
||||
("macos", "x86_64"),
|
||||
("unknown", "x86_64"),
|
||||
] {
|
||||
assert!(ExportRequest::validate(
|
||||
&app.project,
|
||||
Artifact::Executable,
|
||||
system,
|
||||
arch,
|
||||
&Destination::new(t.0.join("out"))
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
app.runtime_dir = t.0.join("missing-runtimes");
|
||||
menu(&mut app, Command::MakeExe);
|
||||
let out = t.0.join("out");
|
||||
field(&mut app, 3, &out.display().to_string());
|
||||
plain(&mut app, K::Enter);
|
||||
let status = await_export(&mut app);
|
||||
assert!(
|
||||
matches!(&status, ExportStatus::Failed(e) if e.contains("Runtime-Vorlage fehlt")),
|
||||
"{status:?}"
|
||||
);
|
||||
assert!(!out.exists());
|
||||
assert!(app.dialog.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debugger_reports_absent_library_source_and_can_step_back_to_caller() {
|
||||
let t = Temp::new();
|
||||
let path = t.0.join("lib.bas");
|
||||
fs::write(&path, "SUB Answer\nPRINT 42\nEND SUB\n").unwrap();
|
||||
let mut compiler = tb_vm::project::ProjectCompiler::default();
|
||||
compiler.debug_symbols = true;
|
||||
let bytes = tb_vm::project_io::SourceLoader::default()
|
||||
.load(&path)
|
||||
.unwrap()
|
||||
.compile_library(&mut compiler)
|
||||
.unwrap()
|
||||
.to_tbl()
|
||||
.unwrap();
|
||||
fs::write(t.0.join("lib.tbl"), bytes).unwrap();
|
||||
fs::remove_file(&path).unwrap();
|
||||
fs::write(t.0.join("main.bas"), "CALL Answer\nPRINT 9\nEND\n").unwrap();
|
||||
fs::write(t.0.join("main.mak"), "main.bas\nlib.tbl\n").unwrap();
|
||||
let mut app = t.app();
|
||||
app.load_initial_project(t.0.join("main.mak")).unwrap();
|
||||
let documents = app.project.documents().count();
|
||||
let mut missing = false;
|
||||
for _ in 0..10 {
|
||||
app.execute(Command::Step);
|
||||
for n in 0..100 {
|
||||
app.tick_execution(n);
|
||||
if app.execution != Execution::Running {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if app.message.contains("Bibliotheksquelle nicht verfügbar") {
|
||||
missing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(missing, "{}", app.message);
|
||||
assert_eq!(app.project.documents().count(), documents);
|
||||
assert!(!path.exists());
|
||||
app.execute(Command::ProcedureStep);
|
||||
for n in 0..100 {
|
||||
app.tick_execution(n);
|
||||
if app.execution != Execution::Running {
|
||||
break;
|
||||
}
|
||||
}
|
||||
app.execute(Command::Continue);
|
||||
for n in 0..100 {
|
||||
app.tick_execution(n);
|
||||
if app.execution == Execution::Ended {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(app.execution, Execution::Ended, "{}", app.message);
|
||||
assert_eq!(
|
||||
tb_runtime::snapshot::text(app.session.screen()),
|
||||
" 42 \n 9 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalization_failure_is_reported_and_preserves_existing_output() {
|
||||
let t = Temp::new();
|
||||
let mut app = t.app();
|
||||
let target = tb_export::Target::MacosArm64;
|
||||
app.runtime_dir = t.0.join("runtimes");
|
||||
let template = app
|
||||
.runtime_dir
|
||||
.join(target.triple())
|
||||
.join(target.runtime_name());
|
||||
fs::create_dir_all(template.parent().unwrap()).unwrap();
|
||||
// Header für den negativen Finalisierungstest, kein ausführbarer Zielnachweis.
|
||||
let mut header = vec![0; 256];
|
||||
header[..4].copy_from_slice(&[0xcf, 0xfa, 0xed, 0xfe]);
|
||||
header[4..8].copy_from_slice(&0x0100000cu32.to_le_bytes());
|
||||
header[12..16].copy_from_slice(&2u32.to_le_bytes());
|
||||
header.extend_from_slice(&tb_export::runtime_marker(target));
|
||||
fs::write(&template, header).unwrap();
|
||||
tb_export::prepare_template(&template, target).unwrap();
|
||||
menu(&mut app, Command::MakeExe);
|
||||
for (n, value) in [(1, "macos"), (2, "aarch64")] {
|
||||
while app.dialog.as_ref().unwrap().focus != n {
|
||||
plain(&mut app, K::Tab);
|
||||
}
|
||||
while app.dialog.as_ref().unwrap().fields[n].string() != value {
|
||||
plain(&mut app, K::Right);
|
||||
}
|
||||
}
|
||||
let out = t.0.join("existing");
|
||||
fs::write(&out, b"old").unwrap();
|
||||
field(&mut app, 3, &out.display().to_string());
|
||||
plain(&mut app, K::Tab);
|
||||
plain(&mut app, K::Char(' '));
|
||||
plain(&mut app, K::Enter);
|
||||
let status = await_export(&mut app);
|
||||
assert!(
|
||||
matches!(&status,ExportStatus::Failed(e) if e.contains("Finalisierung")),
|
||||
"{status:?}"
|
||||
);
|
||||
assert_eq!(fs::read(&out).unwrap(), b"old");
|
||||
assert!(fs::read_dir(&t.0).unwrap().all(|p| !p
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.starts_with(".tb-export-")));
|
||||
}
|
||||
|
||||
@@ -488,3 +488,51 @@ fn cursor_after_shortening_uses_original_position_and_open_after_save_uses_fresh
|
||||
.unwrap());
|
||||
assert_eq!(p.document(p.members()[0]).unwrap().code(), "PRINT 7\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libraries_are_binary_members_with_relative_save_as_and_no_source_views() {
|
||||
let t = Temp::new();
|
||||
let lib = t.write("lib.bas", "SUB Answer\nPRINT 42\nEND SUB\n");
|
||||
let mut compiler = tb_vm::project::ProjectCompiler::default();
|
||||
let bytes = SourceLoader::default()
|
||||
.load(&lib)
|
||||
.unwrap()
|
||||
.compile_library(&mut compiler)
|
||||
.unwrap()
|
||||
.to_tbl()
|
||||
.unwrap();
|
||||
let tbl = t.0.join("lib.tbl");
|
||||
fs::write(&tbl, &bytes).unwrap();
|
||||
fs::remove_file(lib).unwrap();
|
||||
let main = t.write("main.bas", "CALL Answer\nEND\n");
|
||||
let mak = t.write("p.mak", "lib.tbl\nmain.bas\n'$STARTUP: \"main.bas\"\n");
|
||||
let mut p = Project::open(&mak, vec![]).unwrap();
|
||||
let id = p.members()[0];
|
||||
assert!(p.document(id).unwrap().is_library());
|
||||
assert!(p.open_view(id).is_err());
|
||||
assert!(p.replace_text(id, 0..0, "bad").is_err());
|
||||
assert!(p.save_file(id, None).is_err());
|
||||
assert!(p.set_startup(Some(id)).is_err());
|
||||
assert_eq!(p.source_members().len(), 1);
|
||||
p.sources()
|
||||
.unwrap()
|
||||
.compile(&mut compiler, "consumer")
|
||||
.unwrap();
|
||||
fs::create_dir(t.0.join("sub")).unwrap();
|
||||
p.save_project(&t.plan("sub/p.mak")).unwrap();
|
||||
let text = fs::read_to_string(t.0.join("sub/p.mak")).unwrap();
|
||||
assert!(text.contains("../lib.tbl"), "{text}");
|
||||
assert_eq!(fs::read(&tbl).unwrap(), bytes);
|
||||
let mut reopened = Project::open(&t.0.join("sub/p.mak"), vec![]).unwrap();
|
||||
let id = reopened.members()[0];
|
||||
reopened.remove_file(id, None).unwrap();
|
||||
assert!(tbl.exists());
|
||||
reopened.add_file(&tbl).unwrap();
|
||||
assert_eq!(reopened.manifest().members().last().unwrap(), &tbl);
|
||||
assert!(reopened.find_document(&main).is_some());
|
||||
reopened
|
||||
.sources()
|
||||
.unwrap()
|
||||
.compile(&mut compiler, "consumer")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -305,7 +305,8 @@ fn twenty_back_steps_end_of_contents_and_all_entries() {
|
||||
h.back();
|
||||
assert_eq!(h.position, p);
|
||||
assert!(h.notice.contains("20"));
|
||||
h.visit(11, 0, None);
|
||||
let (last, _) = Catalog::embedded().resolve("", "PLAN.md").unwrap();
|
||||
h.visit(last, 0, None);
|
||||
let p = h.position.clone();
|
||||
h.next();
|
||||
assert_eq!(h.position, p);
|
||||
|
||||
@@ -101,31 +101,42 @@ Externe Änderungen der Optionsdatei werden vor dem Ersetzen erkannt.
|
||||
|
||||
## Native Exporte: UI in Phase 5, Erzeugung in Phase 6
|
||||
|
||||
Run → Make EXE File… (Alt+R, E) und Make Library… (Alt+R, L) öffnen
|
||||
vollständig bedienbare Dialoge. Artefaktart ist das native eigenständige
|
||||
Executable bzw. die native Systembibliothek. Zielsystem (Linux/macOS/Windows)
|
||||
und Architektur (x86_64/aarch64) sind auswählbar; Vorgabe ist das laufende
|
||||
Zielsystem. Ausgabepfad und Überschreibentscheidung werden je Auftrag
|
||||
erfasst. Die Auswahl ist noch keine Zusage einer vom Backend unterstützten
|
||||
Kombination; die Phase-6-Plattformmatrix bestätigt die tatsächlichen Ziele.
|
||||
Run → Make EXE File… (Alt+R, E) erzeugt ein eigenständiges natives
|
||||
Executable. Unterstützt werden Windows x86_64, macOS aarch64 und Linux
|
||||
x86_64/aarch64. Die passende `tbrt`-Vorlage samt `.meta` muss neben dem
|
||||
IDE-Executable unter `runtimes/<target-triple>/` liegen. Fehlende Vorlagen
|
||||
und Finalisierungsvoraussetzungen werden konkret gemeldet; macOS-Exporte
|
||||
benötigen macOS mit `/usr/bin/codesign`.
|
||||
|
||||
Prüfen/Enter validiert die Angaben und schützt Projektdatei, geöffnete
|
||||
Dokumente und binäre FRM-Originale. Leere/ungültige Pfade, fehlende
|
||||
Zielverzeichnisse, bestehende Ziele ohne Zustimmung und `.tbc` als natives
|
||||
Ziel ergeben eine Diagnose; die anderen Angaben bleiben erhalten.
|
||||
Make Library… (Alt+R, L) erzeugt eine portable `.tbl`. Hier gibt es keine
|
||||
System-/Architekturwahl und keine erforderliche Runtime-Vorlage. Add File
|
||||
nimmt TBL als Binärabhängigkeit auf, Remove File entfernt nur den Verweis.
|
||||
MAK-Speicherung und Save As erhalten die Reihenfolge und relativen Verweise.
|
||||
Bibliotheken sind weder editierbare Quelltexte noch zulässige Startdateien.
|
||||
Start, Check, Debugger und Export verwenden denselben Linker wie `tbc link`.
|
||||
Fehlende Bibliotheksquellen werden im Debugger benannt; Fortsetzen und
|
||||
Prozedurschritte bleiben möglich.
|
||||
|
||||
Nur **Erzeugen** ist bis Phase 6 gesperrt, mit dem Hinweis „Native
|
||||
Erzeugung folgt in Phase 6“. Ein Produktionsaufruf schreibt keine Datei
|
||||
und meldet keinen erfundenen Erfolg. Der vorbereitete Auftrag trägt
|
||||
Projektidentität und Revisionsstand, Artefakt, Zielsystem, Architektur,
|
||||
Pfad und Überschreibentscheidung. Die UI kann zu diesem Auftrag Laufend,
|
||||
Erfolg, Fehler und Abbruch darstellen. Oberflächentests speisen diese
|
||||
Ergebnisse kontrolliert ein; sie belegen keine native Erzeugung.
|
||||
Erzeugen/Enter beziehungsweise der gleichnamige Mausknopf validiert den
|
||||
Ausgabepfad und die Überschreibentscheidung. Projektdateien, Quelltexte,
|
||||
Includes, binäre FRM-Originale und Bibliotheken sind geschützt. Leere oder
|
||||
ungültige Pfade, fehlende Zielverzeichnisse, unzulässige EXE-Ziele und `.tbc`
|
||||
als natives Exportziel liefern eine Diagnose und erhalten die Angaben.
|
||||
|
||||
Die tatsächliche Erzeugung und reale Backend-Anbindung, native Formate,
|
||||
Library-Symbole/Aufrufvertrag und der native Verbraucher-Nachweis stehen
|
||||
explizit in [PLAN.md, Phase 6](../PLAN.md). Ein umbenanntes TBC erfüllt
|
||||
keines der beiden Exportziele.
|
||||
Der Export verwendet den aktuellen Modul-/Form-/Include-Pufferstand ohne
|
||||
implizites Speichern. Bibliotheksbytes gehören zum Auftragsstand. Während
|
||||
der Hintergrundarbeit bleibt der Dialog bedienbar; Esc verwirft den Auftrag
|
||||
und seine temporäre Ausgabe. Erfolg erscheint erst nach Veröffentlichung.
|
||||
Projektänderungen, ausgetauschte Libraries, neuere Aufträge und extern
|
||||
geänderte Ziele verhindern die Veröffentlichung eines alten Ergebnisses.
|
||||
Das Schließen eines bereits erfolgreichen Dialogs macht den Erfolg nicht
|
||||
rückwirkend zu einem Abbruch.
|
||||
|
||||
Absichtlich backendlose Oberflächentests zeigen weiterhin die
|
||||
Phase-6-Abhängigkeit und erzeugen keine Dateien. Die echte Integration wird
|
||||
zusätzlich mit nativer Ausführung und einem quellfreien TBL-Verbraucher
|
||||
geprüft. Format und CLI-Beispiele: [P-Code-Bibliotheken](pcode-bibliotheken.md).
|
||||
Die vollständige Zielsystem-/Terminalmatrix folgt gemäß [PLAN.md](../PLAN.md).
|
||||
|
||||
## Befehlsabdeckung nach Change 07
|
||||
|
||||
@@ -141,7 +152,7 @@ Output-Fokus unterbricht Ctrl+C sofort die Sitzung, ohne eine BASIC-Taste einzur
|
||||
| Edit | Undo, Cut/Copy/Paste/Clear, New Sub/Function, Event Procedures | — |
|
||||
| View | Code/Form-Auswahl, Prozedurauswahl, Included File/Lines, Diagnostics, Designer Menu Bar/Value-Fokus, Grid Lines, Output Screen, Next Statement | — |
|
||||
| Search | Find, Selected Text, Repeat Last Find, Change | — |
|
||||
| Run | Start/Restart/Continue/Pause, COMMAND$, wirksame Startdatei und beide Make-Dialoge | Native Erzeugung: Phase 6 |
|
||||
| Run | Start/Restart/Continue/Pause, COMMAND$, wirksame Startdatei und beide Make-Dialoge | Echte EXE-/TBL-Erzeugung; Vierzielabnahme in Phase 6 |
|
||||
| Debug | Breakpoints, Schritte, Calls, Watches/Watchpoints, Direktfenster, Trace/History, Break on Errors und Set Next Statement | — |
|
||||
| Options | Display, Set Paths, Right Mouse, Save, Syntax Checking einschließlich Zeilenprüfung | — |
|
||||
| Window | New Window, Arrange All, Calls/Debug/Help/Immediate/Output/Project, aktive Codeliste, alle Fensterkürzel | — |
|
||||
|
||||
@@ -98,7 +98,10 @@ protected }, cancelled)` erhält den fertig verknüpften Projektstand, keine
|
||||
IDE-Dokumenttypen. `protected` enthält insbesondere das Eingabemanifest;
|
||||
Quell-/Include-Pfade aus dem Kompilat sowie Vorlage/Metadaten ergänzt der
|
||||
Export selbst. Die IDE kann ihren bestehenden Abbruch-/Revisionszustand
|
||||
über den Callback prüfen. `publish` stellt denselben Schutz für TBL/Link
|
||||
vor der Veröffentlichung prüfen: `prepare_export` beziehungsweise
|
||||
`prepare_publication` liefern ein fertiges `PreparedPublication`. Der
|
||||
UI-Thread prüft Auftrag und Eingaben, bevor er dessen `publish` aufruft;
|
||||
Drop verwirft das Staging. `publish` stellt denselben Schutz für TBL/Link
|
||||
bereit: temporäre Geschwisterdatei, vollständige Finalisierung, sync,
|
||||
Abbruch- und Zielkonfliktprüfung, danach Veröffentlichung. Ein neues Ziel
|
||||
wird mit atomarem No-clobber angelegt. Ein vorhandenes Ziel wird nur nach
|
||||
|
||||
@@ -148,10 +148,10 @@ Dateieffekt und Neustart durch `RUN` werden geprüft. Die JSON-Ausgabe nennt
|
||||
Host-Target, Prüfsumme und Ergebnis. Ein Cross-Compile ersetzt diesen Lauf
|
||||
nicht. Die vier JSON-Nachweise gehören in die Plattform-/Gitea-Abnahme.
|
||||
|
||||
IDE-Change 04 kann `ProjectSources::compile_library`, `compile` und
|
||||
`protected_inputs` sowie `ProjectCompiler::link_library` nutzen. Er ergänzt
|
||||
das IDE-Dokumentmodell für nicht editierbare TBL-Mitglieder und verbindet
|
||||
Make Library/Make EXE mit derselben Veröffentlichung; die UI ist Teil von 04.
|
||||
Die IDE nutzt denselben Compiler-/Linkdienst für Start, Check, Debugger,
|
||||
Make Library und Make EXE. TBL-Mitglieder sind nicht editierbar. Die Make-
|
||||
Dialoge verwenden ungespeicherte Puffer und veröffentlichen nur weiterhin
|
||||
gültige Aufträge; [Bedienung und Voraussetzungen](ide-bedienung.md#native-exporte-ui-in-phase-5-erzeugung-in-phase-6).
|
||||
Change 05 führt den identischen Verbrauchercheck auf allen vier echten
|
||||
Zielsystemen aus. Change 06 baut die vier nativen Compiler-/Runtime-Pakete
|
||||
zentral auf dem Linux-arm64-Gitea-Runner per Cross-Kompilierung und bindet
|
||||
|
||||
@@ -54,10 +54,12 @@ erklärt Fehlerhalte, Watchpoints und die Grenzen von Set Next Statement.
|
||||
|
||||
## Speichern und nächste Schritte
|
||||
|
||||
File → Save Project hält den Stand fest. Make EXE File und Make Library
|
||||
bieten bereits die Zielauswahl; die tatsächliche Erzeugung eines nativen
|
||||
Standalone-Programms beziehungsweise einer nativen Library folgt in Phase 6.
|
||||
Siehe [Exportgrenze](ide-bedienung.md#native-exporte-ui-in-phase-5-erzeugung-in-phase-6).
|
||||
File → Save Project hält den Stand fest. Make EXE File erzeugt mit der
|
||||
passenden Runtime-Vorlage ein natives Standalone-Programm. Make Library
|
||||
erzeugt ohne native Zielwahl eine portable `.tbl` zur Wiederverwendung über
|
||||
`tbc link` oder Add File in einem anderen Projekt. Beide Exporte übernehmen
|
||||
auch ungespeicherte Bearbeitungen; Esc bricht die Erzeugung ab.
|
||||
Siehe [Export und Voraussetzungen](ide-bedienung.md#native-exporte-ui-in-phase-5-erzeugung-in-phase-6).
|
||||
|
||||
Weiter: [Tastatur](tastatur.md), [Hilfe benutzen](hilfe.md),
|
||||
[Sprachreferenz](sprachreferenz.md), [Bibliothek](bibliothek.md).
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
## 1. Auftrag und Backend
|
||||
|
||||
- [ ] 1.1 Bestehenden ExportRequest mit dem gemeinsamen Dienst aus 02/03 verbinden; ein Auftrag muss den aktuellen Modul-/Form-/Include-Overlaystand ohne implizites Speichern erfassen und per Test belegen.
|
||||
- [ ] 1.2 Zielauswahl aus dem gemeinsamen Vierziel-Katalog ableiten; App-Tests müssen ungültige EXE-Kombinationen, fehlende tbrt-Vorlagen und Finalisierung diagnostizieren sowie TBL-Ausgabe ohne native Zielauswahl/Vorlage ermöglichen.
|
||||
- [ ] 1.3 Langlaufende Erzeugung mit eindeutiger Auftragsgeneration und temporärem Ergebnis an den Terminaltreiber anbinden; ein verzögerter Testauftrag muss weitere UI-Eingaben zulassen, ohne lebende Rc-/VM-Werte zu übertragen.
|
||||
- [x] 1.1 Bestehenden ExportRequest mit dem gemeinsamen Dienst aus 02/03 verbinden; ein Auftrag muss den aktuellen Modul-/Form-/Include-Overlaystand ohne implizites Speichern erfassen und per Test belegen.
|
||||
- [x] 1.2 Zielauswahl aus dem gemeinsamen Vierziel-Katalog ableiten; App-Tests müssen ungültige EXE-Kombinationen, fehlende tbrt-Vorlagen und Finalisierung diagnostizieren sowie TBL-Ausgabe ohne native Zielauswahl/Vorlage ermöglichen.
|
||||
- [x] 1.3 Langlaufende Erzeugung mit eindeutiger Auftragsgeneration und temporärem Ergebnis an den Terminaltreiber anbinden; ein verzögerter Testauftrag muss weitere UI-Eingaben zulassen, ohne lebende Rc-/VM-Werte zu übertragen.
|
||||
|
||||
- [ ] 1.4 TBL-Mitglieder an Projektverwaltung, MAK-Roundtrip/Save-As und Start/Check/Debugger anbinden; Add/Remove ohne Dateilöschung, unzulässige Library-Startauswahl, quellfreie Prozeduraufrufe und ehrliche Anzeige fehlender Debuggerquellen testen.
|
||||
- [ ] 1.5 TBL-Bytes in Cache-/Auftragsidentität aufnehmen; geänderte Libraries müssen eine Neuverknüpfung erzwingen und während des Exports ausgetauschte Libraries die Veröffentlichung des alten Auftrags verhindern.
|
||||
- [x] 1.4 TBL-Mitglieder an Projektverwaltung, MAK-Roundtrip/Save-As und Start/Check/Debugger anbinden; Add/Remove ohne Dateilöschung, unzulässige Library-Startauswahl, quellfreie Prozeduraufrufe und ehrliche Anzeige fehlender Debuggerquellen testen.
|
||||
- [x] 1.5 TBL-Bytes in Cache-/Auftragsidentität aufnehmen; geänderte Libraries müssen eine Neuverknüpfung erzwingen und während des Exports ausgetauschte Libraries die Veröffentlichung des alten Auftrags verhindern.
|
||||
|
||||
## 2. Abschluss und Abbruch
|
||||
|
||||
- [ ] 2.1 Validierung von Generation, Projektstempel und Abbruch vor Dateiveröffentlichung implementieren; Projektwechsel und konkurrierende Aufträge dürfen weder alte Ziele überschreiben noch fremden Erfolg anzeigen.
|
||||
- [ ] 2.2 Esc/Abbruch und Fehlerbereinigung implementieren; Tests müssen Kindprozessende beziehungsweise verworfenes Spätergebnis, entfernte Staging-Dateien und unveränderte bestehende Ziele nachweisen.
|
||||
- [ ] 2.3 Echte Status-/Ziel-/Diagnoseanzeige und Rückkehr zum Projekt anbinden; Tastatur- und Mauswege müssen denselben Auftrag auslösen und alle Dirty-/Dokumentzustände erhalten.
|
||||
- [x] 2.1 Validierung von Generation, Projektstempel und Abbruch vor Dateiveröffentlichung implementieren; Projektwechsel und konkurrierende Aufträge dürfen weder alte Ziele überschreiben noch fremden Erfolg anzeigen.
|
||||
- [x] 2.2 Esc/Abbruch und Fehlerbereinigung implementieren; Tests müssen Kindprozessende beziehungsweise verworfenes Spätergebnis, entfernte Staging-Dateien und unveränderte bestehende Ziele nachweisen.
|
||||
- [x] 2.3 Echte Status-/Ziel-/Diagnoseanzeige und Rückkehr zum Projekt anbinden; Tastatur- und Mauswege müssen denselben Auftrag auslösen und alle Dirty-/Dokumentzustände erhalten.
|
||||
|
||||
## 3. Integration und Speckonsistenz
|
||||
|
||||
- [ ] 3.1 Backendlose Phase-5-Tests ausdrücklich mit fehlendem Backend weiterführen und zusätzliche reale Exportfälle ergänzen; beide Modi müssen ihre unterschiedlichen Erfolg-/Verfügbarkeitsbedingungen belegen.
|
||||
- [ ] 3.2 Vollständigen IDE-Hauptablauf um tatsächlichen EXE-Start und TBL-Export mit separatem BASIC-Verbraucher über `tbc link` ergänzen; Ausgabe, Änderungen im ungespeicherten Include, IDE-/CLI-Linkparität und erhaltene Quellbytes prüfen.
|
||||
- [ ] 3.3 Alle geänderten und neuen Spec-Szenarien mit Tests zuordnen; native Tests, bestehende IDE-Regressionen, Clippy, Format- und OpenSpec-Prüfung müssen ohne offenen Befund bestehen.
|
||||
- [x] 3.1 Backendlose Phase-5-Tests ausdrücklich mit fehlendem Backend weiterführen und zusätzliche reale Exportfälle ergänzen; beide Modi müssen ihre unterschiedlichen Erfolg-/Verfügbarkeitsbedingungen belegen.
|
||||
- [x] 3.2 Vollständigen IDE-Hauptablauf um tatsächlichen EXE-Start und TBL-Export mit separatem BASIC-Verbraucher über `tbc link` ergänzen; Ausgabe, Änderungen im ungespeicherten Include, IDE-/CLI-Linkparität und erhaltene Quellbytes prüfen.
|
||||
- [x] 3.3 Alle geänderten und neuen Spec-Szenarien mit Tests zuordnen; native Tests, bestehende IDE-Regressionen, Clippy, Format- und OpenSpec-Prüfung müssen ohne offenen Befund bestehen.
|
||||
|
||||
Verifiziert am 2026-09-07: [Nachweise und Szenarioabgleich](verification.md). Keine offenen Befunde im Change-Umfang.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Verifikation: phase-6-04-ide-exportanbindung
|
||||
|
||||
Stand: 2026-09-07. Implementierung auf Basis von `25176947ba992520d6cf451f20097b6380fb9555`.
|
||||
Change 03 wurde zuvor synchronisiert, archiviert und mit diesem Commit auf
|
||||
`origin/main` bestätigt. Change 04 wurde am 2026-09-07 synchronisiert und archiviert; dieser Bericht
|
||||
wird gemeinsam mit der Implementierung committed.
|
||||
|
||||
## Ergebnis
|
||||
|
||||
| Dimension | Ergebnis |
|
||||
|---|---|
|
||||
| Vollständigkeit | 11/11 Aufgaben, 7 Requirements und 11 Szenarien abgedeckt |
|
||||
| Korrektheit | Keine offenen Befunde nach Korrekturen und erneuten Prüfungen |
|
||||
| Kohärenz | Bestehende Dokumentidentitäten, gemeinsamer Projektcompiler/Linker und Exportdienst; nur Bytes und Auftragsdaten im Worker |
|
||||
| Zielabnahme | Echte native Ausführung auf macOS arm64 bestanden; vollständige Plattform-/Releaseabnahme bleibt gemäß PLAN in 05/06 |
|
||||
|
||||
## Requirement- und Szenarioabgleich
|
||||
|
||||
| Requirement / Szenario | Umsetzung und Nachweis |
|
||||
|---|---|
|
||||
| Reale Erzeugung / ungespeichertes Include | `export.rs::Job::start` übersetzt den aktuellen `Project::sources`-Overlaystand ohne Speichern, übergibt TBC-/TBL-Bytes an den Worker. App-Test `real_library_export_uses_dirty_include_and_mouse_without_runtime_template` und echter CLI-/IDE-Abnahmetest `real_native_ide_exports_and_source_free_cli_library_consumer` prüfen geändertes Include, unveränderte Quelldateien, Dirty-Status und tatsächliches Ergebnis. |
|
||||
| Verfügbarkeit und Zielkonsistenz / fehlende Vorlage | Zielkombinationen stammen aus `tb_export::Target::ALL`. EXE-Validierung weist Windows arm64 und macOS amd64 ab; fehlende Vorlagen und fehlgeschlagene Finalisierung erscheinen im bedienbaren Dialog. App-Tests `executable_target_validation_and_missing_template_are_honest` und `finalization_failure_is_reported_and_preserves_existing_output`. |
|
||||
| Abbruch und eindeutige Veröffentlichung / Ergebnis nach Projektwechsel | `Job` liefert `PreparedPublication`; `App::tick_export` prüft Dialog, Generation, Projekt-/Include-/TBL-Stand und bisherigen Zielinhalt vor Veröffentlichung im UI-Thread. Drop verwirft Staging, setzt Abbruch und beendet den Worker; codesign wird bei Abbruch beendet. Kontrollierte verzögerte Resultate prüfen Resize während der Arbeit, Esc, Projektwechsel, neueren Auftrag, Bearbeitung und externe Zieländerung. Kein alter Auftrag veröffentlicht. |
|
||||
| Bedienung bis unabhängige Nutzung / IDE-Export benutzen | Tastatur- und Mauswege verwenden denselben Submit-Pfad. Echte native Abnahme erzeugt ein Forms-Projekt über den IDE-Hauptablauf, hält im Debugger, exportiert EXE und TBL, entfernt Library-Quellen, verwendet die TBL über den echten `tbc link`-Befehl und in einem zweiten IDE-Projekt. CLI-/IDE-Ergebnisse stimmen überein. Das native Programm startet anschließend mit leerem PATH und zeigt die ungespeicherte Include-Änderung einschließlich Dateieffekt. |
|
||||
| Bibliotheken im IDE-Projekt / nach Lauf ersetzen | TBL wird als validierte Binärabhängigkeit mit Dokumentidentität geführt, jedoch ohne Textansicht, Editieren, Text-/Dateispeicherung oder Startup-Auswahl. Loader überspringt TBL-Textoverlays; Start/Check/Debugger verwenden den gemeinsamen Linker. Dokumenttest prüft Add/Remove, Reihenfolge, relative MAK-Referenzen, Save As und erhaltene Librarydatei. `replaced_library_invalidates_pending_export_even_with_same_length` prüft Neuübersetzung und verworfenen Export trotz gleicher Dateigröße. |
|
||||
| Bibliotheken im IDE-Projekt / Library ohne native Vorlage | Make Library hat nur Artefaktbeschreibung, `.tbl`-Ausgabepfad und Überschreibentscheidung. Reale quellfreie Verwendung aus dem App-Test funktioniert ohne Runtime-Vorlage. |
|
||||
| Vollständige Exportdialoge / ohne Backend | Bestehender App-Oberflächentest und Phase-5-Hauptablauf setzen ausdrücklich `export_backend=false`; Verfügbarkeitsgrund, Angaben, Validierung und unveränderte Dateien werden weiter geprüft. |
|
||||
| Vollständige Exportdialoge / fehlerhafte Ausgabeangaben | Leere Pfade, fehlende Zielverzeichnisse, TBC als natives Artefakt, falsche Libraryendung und nicht freigegebene bestehende Ziele werden vor Arbeit abgewiesen. Bestehende App-/CLI-Oberflächentests bleiben erhalten und berücksichtigen die portable Library-Maske. |
|
||||
| Vollständige Exportdialoge / kontrollierte Ergebnisdarstellung | Die backendlosen Tests melden Laufend, Erfolg, Fehler und Abbruch mit demselben Auftrag zurück und prüfen Anzeige sowie unveränderte Projektstempel; sie behaupten keine Dateierzeugung. |
|
||||
| Vollständige Exportdialoge / verfügbares Backend | Zusätzliche reale Library- und native Abnahmen prüfen tatsächliche Dateien, Status und unabhängige Nutzung. Schließen nach erfolgreicher Veröffentlichung erhält den tatsächlichen Erfolg. |
|
||||
| Export-UI als Phasenleistung / ohne Scheinerfolg | Backendlose Phase-5-Abnahme bleibt explizit erhalten; zusätzlich wird die Phase-6-Erzeugung über dieselbe UI nachgewiesen. |
|
||||
|
||||
Zusätzlicher Debuggernachweis: `debugger_reports_absent_library_source_and_can_step_back_to_caller`
|
||||
prüft den tatsächlichen Halt in quellfreiem Librarycode, die Meldung des fehlenden
|
||||
Quelltexts, Prozedurschritt/Fortsetzen und das richtige Ergebnis ohne erfundene
|
||||
Dokumente oder erzeugte Quelldateien.
|
||||
|
||||
## Behobene Befunde
|
||||
|
||||
- Die erste Revisionsprüfung bezog auch externe Änderungen gespeicherter,
|
||||
bereits geöffneter Includes ein. Geöffnete Puffer bleiben jetzt maßgeblich;
|
||||
TBL und nicht geöffnete Dateieingaben werden über ihre tatsächlichen Bytes
|
||||
geprüft. Der bestehende Include-/Editor-Regressionsfall besteht.
|
||||
- Späte Ergebnisse dürfen keine Ausgabewirkung vor dem UI-Check haben:
|
||||
gemeinsame Veröffentlichung in Vorbereitung und Freigabe getrennt;
|
||||
zusätzlich wird der Zielinhalt bereits ab Auftragsbeginn geschützt.
|
||||
- Ein bereits erfolgreicher Export wurde beim Schließen als abgebrochen
|
||||
gespeichert. Der abgeschlossene Erfolgsstatus bleibt jetzt erhalten.
|
||||
- Worker- und Signierprozesslebensdauer sowie Staging-Bereinigung sind an den
|
||||
Auftrag gebunden, auch beim Beenden der IDE.
|
||||
- Bibliotheken sind aus Text-/Startup-Auswahl und Quellspeicherung ausgeschlossen;
|
||||
auch das implizite Ausführungsziel wird aus Quellmitgliedern gewählt.
|
||||
- Alte Exporthinweise in Bedienung/Tutorial und fehlende eingebettete Hilfeziele
|
||||
wurden korrigiert. Die Hilfe-Endprüfung verwendet den aufgelösten letzten
|
||||
Dokumenteintrag statt einer festen Seitennummer.
|
||||
|
||||
## Ausgeführte Prüfungen
|
||||
|
||||
| Prüfung | Ergebnis |
|
||||
|---|---|
|
||||
| `cargo test --locked --workspace` | 633 bestanden, 0 fehlgeschlagen, 3 ausdrücklich ignoriert |
|
||||
| Echter nativer IDE-Abnahmetest mit `TB_IDE_RUNTIME_DIR` und `--ignored` | 1 bestanden auf macOS arm64; IDE→EXE→Prozessstart und IDE→TBL→CLI-/IDE-Verbraucher |
|
||||
| Zusätzlicher finaler IDE-Ausführungstest nach Quellzielkorrektur | 12 bestanden |
|
||||
| Finaler Help-Test nach Dokumentationsabgleich | 11 bestanden; zusätzlich Offline-Kindprozess bestanden |
|
||||
| `cargo clippy --locked --workspace --all-targets -- -D warnings` | Bestanden |
|
||||
| `cargo fmt --all -- --check`, `git diff --check` | Bestanden |
|
||||
| `openspec validate --all --strict` | 30 Elemente bestanden, 0 fehlgeschlagen |
|
||||
| Release-Build von IDE/CLI und nativer Runtime/Exportwerkzeug | Bestanden |
|
||||
| `native-abnahme.py --target aarch64-apple-darwin` | Echte native Programme, Forms, PTY, Exportabbruch, Signierung und Beschädigungsprüfungen bestanden |
|
||||
| `library-abnahme.py` | Identisches gemeinsames TBL-Artefakt, quellfreie TBC-/EXE-Verbraucher und RUN bestanden |
|
||||
| `cargo check --locked --target aarch64-unknown-linux-gnu -p tb-ide` | Bestanden; kein Linux-Ausführungsnachweis |
|
||||
|
||||
Die regulär ignorierten Fälle sind Golden-Erzeugung, externer Fremdbestand und
|
||||
der neue native IDE-Test mit expliziter Runtime-Voraussetzung. Letzterer wurde
|
||||
separat tatsächlich ausgeführt. Ein früher Korpuslauf traf im unveränderten
|
||||
Zeitfall `timezonefallback.bas` eine Sekundengrenze; der vollständige abschließende
|
||||
Workspace-Lauf bestand ohne Anpassung dieses Tests oder seiner Sollwerte.
|
||||
|
||||
Native IDE-Abnahme reproduzieren: geprüfte Host-Vorlage samt Metadaten unter
|
||||
`<runtime-root>/<target-triple>/tbrt[.exe]` bereitstellen (beispielsweise mit
|
||||
`tests/support/build-runtime.py`), `TB_IDE_RUNTIME_DIR=<runtime-root>` setzen und
|
||||
`cargo test --locked -p tb-cli --bin tbc ide_acceptance::real_native_ide_exports_and_source_free_cli_library_consumer -- --ignored`
|
||||
ausführen. Ohne diese explizite Voraussetzung wird kein nativer Nachweis behauptet.
|
||||
|
||||
Rohprotokolle dieser Sitzung: `/tmp/tb04-workspace-final.log`,
|
||||
`/tmp/tb04-native-final.log`, `/tmp/tb04-execution-final.log`, `/tmp/tb04-help.log`,
|
||||
`/tmp/tb04-clippy-final.log`, `/tmp/tb04-openspec.log`,
|
||||
`/tmp/tb04-native-regression.log`, `/tmp/tb04-library-regression.log`,
|
||||
`/tmp/tb04-linux-check.log`, `/tmp/tb04-release-final.log`.
|
||||
|
||||
## Übergabe
|
||||
|
||||
Change 04 wurde nach erfolgreicher Verifikation synchronisiert und archiviert. Change 05 übernimmt
|
||||
die reale Terminalmatrix auf allen vier Zielsystemen, Change 06 den zentralen
|
||||
Cross-Paketbau und paketgebundene Ausführungsnachweise. Der lokale macOS-Nachweis
|
||||
und Linux-Cross-Check ersetzen diese noch ausstehenden Abnahmen nicht.
|
||||
46
openspec/specs/ide-native-exporte/spec.md
Normal file
46
openspec/specs/ide-native-exporte/spec.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# ide-native-exporte Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Verbindet die IDE-Exportdialoge mit der tatsächlichen EXE-/TBL-Erzeugung und dem gemeinsamen P-Code-Linkdienst unter Erhalt von Projektstand, Bedienbarkeit und eindeutigem Auftragsstatus.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Reale Erzeugung aus aktuellem Projektstand
|
||||
Make EXE File und Make Library SHALL bei verfügbaren Voraussetzungen dieselben nativen Executables beziehungsweise portablen TBL-Bibliotheken wie der gemeinsame CLI-Link-/Exportpfad erzeugen. Der Auftrag SHALL einen konsistenten Stand aller verwendeten Projekt-, Formular- und Include-Inhalte einschließlich ungespeicherter Bearbeitungen sowie der eingelesenen TBL-Bibliotheken erfassen. Export SHALL kein implizites Speichern oder Verwerfen von Dokumentänderungen erfordern.
|
||||
|
||||
#### Scenario: Ungespeichertes Include exportieren
|
||||
- **WHEN** ein Include in der IDE geändert, noch nicht gespeichert und anschließend ein nativer Export gestartet wird
|
||||
- **THEN** enthält das Artefakt die Änderung, während die Quelldatei auf dem Datenträger und der Bearbeitungsstatus erhalten bleiben
|
||||
|
||||
### Requirement: Verfügbarkeit und Zielkonsistenz
|
||||
Für Make EXE SHALL die IDE Windows amd64, macOS arm64 und Linux amd64/arm64 entsprechend den tatsächlichen tbrt-Vorlagen und Finalisierungsmöglichkeiten anbieten. Make Library SHALL `.tbl` als plattformunabhängiges Ergebnis ausweisen und ohne native Zielauswahl oder Runtime-Vorlage funktionieren. Fehlende Backend-/Vorlagen-/Systemvoraussetzungen SHALL konkret angezeigt werden. Eine unzulässige Kombination oder TBC-Datei MUST NOT als erfolgreiche native Ausgabe erscheinen.
|
||||
|
||||
#### Scenario: Fehlende Zielvorlage
|
||||
- **WHEN** ein gültiges Releaseziel gewählt wird, dessen Vorlage auf dem Host fehlt
|
||||
- **THEN** bleibt der Dialog bedienbar und erklärt die fehlende Voraussetzung ohne Erzeugung eines anderen Zielformats
|
||||
|
||||
### Requirement: Abbruch und eindeutige Veröffentlichung
|
||||
Die IDE SHALL während der Erzeugung bedienbar bleiben und einen wirksamen Abbruch erlauben. Nur der weiterhin gültige Auftrag SHALL sein fertig validiertes Artefakt veröffentlichen und Erfolg melden. Abgebrochene, überholte oder fremde Ergebnisse MUST NOT ein neueres Ergebnis überschreiben oder als Erfolg des aktuellen Projekts erscheinen; vorhandene Zieldateien SHALL bei solchen Abbrüchen erhalten bleiben.
|
||||
|
||||
#### Scenario: Ergebnis trifft nach Projektwechsel ein
|
||||
- **WHEN** während eines Exports das Projekt gewechselt oder ein neuerer Auftrag begonnen wird und danach ein altes Ergebnis eintrifft
|
||||
- **THEN** wird das alte Ergebnis nicht veröffentlicht und weder dem neuen Projekt noch dessen Dialog als Erfolg zugeordnet
|
||||
|
||||
### Requirement: Bedienung bis zur unabhängigen Nutzung
|
||||
Tastatur- und Mauswege SHALL Export, Fehlerkorrektur und Rückkehr zum Projekt gleichwertig ermöglichen. Der vollständige IDE-Abnahmepfad SHALL tatsächlich erzeugte Executables starten und TBL-Bibliotheken mit einem separaten BASIC-Verbraucher über `tbc link` verwenden.
|
||||
|
||||
#### Scenario: Export aus der IDE benutzen
|
||||
- **WHEN** ein Projekt über den Make-Dialog erfolgreich exportiert wird
|
||||
- **THEN** lässt sich das Ergebnis unabhängig von der IDE nativ ausführen beziehungsweise ohne Library-Quellen in einem anderen BASIC-Projekt verknüpfen und die Dokumente bleiben unverändert bearbeitbar
|
||||
|
||||
### Requirement: Bibliotheken im IDE-Projekt
|
||||
Die IDE SHALL TBL-Mitglieder über Add File/Remove File verwalten und ihre Reihenfolge und relativen Verweise beim MAK-Speichern erhalten. Bibliotheken MUST NOT als editierbarer Quelltext oder als Startdatei angeboten werden. Start, Check, Debugger und Export SHALL dieselbe Bibliotheksauflösung wie `tbc link` verwenden. Geänderte Bibliotheksinhalte SHALL veraltete Compiler-/Linkergebnisse und ausstehende Exportaufträge invalidieren.
|
||||
|
||||
#### Scenario: Bibliothek nach einem Lauf ersetzen
|
||||
- **WHEN** eine TBL im Projekt nach einem Lauf durch eine geänderte Bibliothek ersetzt und das Projekt erneut gestartet wird
|
||||
- **THEN** nutzt die IDE deren aktuelle Deklarationen und P-Code; ein noch ausstehender Export mit den alten Bytes wird nicht als aktueller Stand veröffentlicht
|
||||
|
||||
#### Scenario: Portable Library ohne native Vorlage
|
||||
- **WHEN** Make Library für ein gültiges Projekt ohne verfügbare tbrt-Zielvorlage verwendet wird
|
||||
- **THEN** erzeugt die IDE dennoch eine nutzbare `.tbl` ohne System-/Architekturwahl; ein späterer EXE-Export prüft seine eigenen Zielvoraussetzungen
|
||||
@@ -57,11 +57,11 @@ Die IDE SHALL die aktuelle Terminalfläche nutzen und auf Größenänderungen re
|
||||
- **THEN** kehrt die bedienbare Oberfläche mit erhaltenen Dokumenten und erreichbaren Fenstern zurück
|
||||
|
||||
### Requirement: Vollständige Exportdialoge mit getrennter Erzeugung
|
||||
Run→Make EXE File und Run→Make Library SHALL bereits in Phase 5 bedienbare Dialoge für Artefaktart, Zielsystem/-architektur und Ausgabepfad öffnen. Die Ziele SHALL als natives eigenständiges Executable beziehungsweise native Systembibliothek beschrieben sein. Die Dialoge SHALL Eingaben prüfen, Überschreibkonflikte behandeln und Zustände für Erzeugung, Erfolg, Fehler und Abbruch darstellen können. Die tatsächliche Erzeugung SHALL erst mit dem Backend aus Phase 6 verfügbar sein; bis dahin SHALL nur die Erzeugungsaktion mit einem verständlichen Verfügbarkeitsgrund gesperrt sein. Die IDE MUST NOT einen Export vortäuschen oder ein bloßes TBC-Kompilat als natives Artefakt ausgeben.
|
||||
Run→Make EXE File und Run→Make Library SHALL bereits in Phase 5 bedienbare Dialoge für Artefaktart, Zielsystem/-architektur und Ausgabepfad öffnen. Mit Phase 6 SHALL die Ziele als natives eigenständiges Executable mit nativer Runtime in tbrt beziehungsweise portable P-Code-Bibliothek `.tbl` beschrieben sein. System-/Architekturwahl SHALL nur für EXE erforderlich sein; Make Library SHALL ohne native Zielvorlage verfügbar sein, sobald sein Backend vorhanden ist. Die Dialoge SHALL Eingaben prüfen, Überschreibkonflikte behandeln und Zustände für Erzeugung, Erfolg, Fehler und Abbruch darstellen können. Die tatsächliche Erzeugung SHALL erst mit dem Backend aus Phase 6 verfügbar sein; bis dahin SHALL nur die Erzeugungsaktion mit einem verständlichen Verfügbarkeitsgrund gesperrt sein. Die IDE MUST NOT einen Export vortäuschen oder ein bloßes TBC-Kompilat als natives Artefakt ausgeben.
|
||||
|
||||
#### Scenario: Dialoge vor dem Erzeugungsbackend
|
||||
- **WHEN** beide Make-Befehle in Phase 5 über Menü oder Tastatur geöffnet werden
|
||||
- **THEN** lassen sich Zielsystem, Architektur und Ausgabepfad bearbeiten und validieren; die fehlende Erzeugung wird erklärt und keine Zieldatei geschrieben
|
||||
- **WHEN** beide Make-Befehle ohne verfügbares Erzeugungsbackend über Menü oder Tastatur geöffnet werden
|
||||
- **THEN** lassen sich die zur Artefaktart passenden Angaben (EXE-Zielsystem/-architektur und Ausgabepfad, für TBL der Ausgabepfad) bearbeiten und validieren; die fehlende Erzeugung wird erklärt und keine Zieldatei geschrieben
|
||||
|
||||
#### Scenario: Fehlerhafte Ausgabeangaben
|
||||
- **WHEN** ein leerer oder unzulässiger Ausgabepfad beziehungsweise ein bestehendes Ziel ohne Überschreibentscheidung angegeben wird
|
||||
@@ -70,3 +70,7 @@ Run→Make EXE File und Run→Make Library SHALL bereits in Phase 5 bedienbare D
|
||||
#### Scenario: Ergebnisdarstellung über die vorbereitete Übergabe
|
||||
- **WHEN** die Exportanbindung im Oberflächentest Erzeugung, Erfolg, Fehler und Abbruch mit demselben gültigen Auftrag zurückmeldet
|
||||
- **THEN** zeigt die IDE jeweils den passenden Zustand mit Ziel beziehungsweise Diagnose und erhält die Projektdokumente; der Test behauptet keine tatsächliche native Dateierzeugung
|
||||
|
||||
#### Scenario: Verfügbares Erzeugungsbackend
|
||||
- **WHEN** ein gültiger Auftrag bei verfügbarem Backend tatsächlich erfolgreich erzeugt wird
|
||||
- **THEN** zeigt der Dialog den bestätigten Erfolg mit dem passenden EXE- oder TBL-Zielartefakt und erhält die Projektdokumente
|
||||
|
||||
@@ -35,8 +35,8 @@ Phase 5 SHALL erst als umgesetzt dokumentiert werden, wenn die Change-Szenarien,
|
||||
- **THEN** verweisen alle Phase-5-Checkboxen auf bestandene Nachweise und der Bericht unterscheidet die lokale headless Abnahme von der weiterhin separat geplanten Plattformmatrix aus Phase 6
|
||||
|
||||
### Requirement: Export-UI als eigene Phasenleistung
|
||||
Die Phase-5-Abnahme SHALL die vollständige Bedienung von Make EXE File und Make Library einschließlich Zielsystem/-architektur, Ausgabepfad, Validierung, Überschreibentscheidung sowie Status-/Fehler-/Abbruchdarstellung prüfen. Dabei SHALL die noch nicht verfügbare native Erzeugung aus Phase 6 ausdrücklich erkennbar sein. Die Abnahme SHALL Oberflächennachweise von der späteren tatsächlichen Erzeugung eigenständiger nativer Executables und nativer Systembibliotheken unterscheiden.
|
||||
Die Phase-5-Abnahme SHALL die vollständige Bedienung von Make EXE File und Make Library einschließlich Zielsystem/-architektur, Ausgabepfad, Validierung, Überschreibentscheidung sowie Status-/Fehler-/Abbruchdarstellung prüfen. Solange das jeweilige Erzeugungsbackend fehlt, SHALL dessen Nichtverfügbarkeit ausdrücklich erkennbar sein. Bei verfügbarem Backend SHALL die Integration zusätzlich echte EXE-/TBL-Artefakte nachweisen und die native Zielwahl auf EXE beschränken. Die Abnahme SHALL Oberflächennachweise von der späteren tatsächlichen Erzeugung eigenständiger nativer Executables und portabler TBL-Bibliotheken mit quellfreier Wiederverwendung unterscheiden.
|
||||
|
||||
#### Scenario: Exportvorbereitung ohne Scheinerfolg
|
||||
- **WHEN** beide Exportdialoge im durchgängigen IDE-Ablauf mit gültigen und ungültigen Angaben bedient werden
|
||||
- **WHEN** beide Exportdialoge ohne verfügbares Erzeugungsbackend im durchgängigen IDE-Ablauf mit gültigen und ungültigen Angaben bedient werden
|
||||
- **THEN** funktionieren Eingabe, Validierung und Rückkehr zum Projekt, die Erzeugungsaktion nennt die Phase-6-Abhängigkeit und weder eine falsche Erfolgsmeldung noch eine als natives Artefakt ausgegebene TBC-Datei entsteht
|
||||
|
||||
Reference in New Issue
Block a user