Phase 6: IDE-Exportanbindung abschließen und Change archivieren
This commit is contained in:
@@ -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);
|
||||
d.export_status = ExportStatus::Ready;
|
||||
d.error = crate::export::UNAVAILABLE.into();
|
||||
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,13 +1739,16 @@ 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 {
|
||||
self.last_export = Some((request, ExportStatus::Cancelled));
|
||||
if d.export_status != ExportStatus::Success {
|
||||
self.last_export = Some((request, ExportStatus::Cancelled));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
)?)?;
|
||||
loaded(loader.load(&path))?;
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user