Phase 6: IDE-Exportanbindung abschließen und Change archivieren
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user