447 lines
16 KiB
Rust
447 lines
16 KiB
Rust
//! Revisionsgebundene Exportaufträge und abwerfbare Hintergrundresultate.
|
|
use crate::documents::{Destination, DocumentId, Project};
|
|
use anyhow::{ensure, Result};
|
|
use std::path::PathBuf;
|
|
use tb_vm::project_io::{has_extension, Manifest};
|
|
|
|
pub const UNAVAILABLE: &str = "Native Erzeugung folgt in Phase 6";
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Artifact {
|
|
Executable,
|
|
Library,
|
|
}
|
|
impl Artifact {
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
Self::Executable => "Natives Standalone-Executable",
|
|
Self::Library => "Portable P-Code-Bibliothek (.tbl)",
|
|
}
|
|
}
|
|
}
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
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 {
|
|
pub fn capture(project: &Project) -> Self {
|
|
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()))
|
|
.collect(),
|
|
}
|
|
}
|
|
}
|
|
#[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(
|
|
project: &Project,
|
|
artifact: Artifact,
|
|
system: &str,
|
|
architecture: &str,
|
|
destination: &Destination,
|
|
) -> Result<Self> {
|
|
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,
|
|
})
|
|
}
|
|
}
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum ExportStatus {
|
|
Ready,
|
|
Running,
|
|
Success,
|
|
Failed(String),
|
|
Cancelled,
|
|
}
|
|
impl ExportStatus {
|
|
pub fn text(&self) -> String {
|
|
match self {
|
|
Self::Ready => "Geprüft".into(),
|
|
Self::Running => "Erzeugung läuft".into(),
|
|
Self::Success => "Erfolgreich erzeugt".into(),
|
|
Self::Failed(e) => format!("Erzeugung fehlgeschlagen: {e}"),
|
|
Self::Cancelled => "Erzeugung abgebrochen".into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
}
|