mirror of
https://github.com/zhom/banderole.git
synced 2026-10-22 12:58:24 +02:00
fix: update deps, fix windows
This commit is contained in:
1 parent
c1183321f5
commit
d97c33c66b
10 files changed
+691
-887
No files matched your search
Generated
+393
-538
File diff suppressed because it is too large.
Load diff
+8
-7
@@ -7,6 +7,7 @@ license = "MIT"
|
||||
authors = ["zhom <contact@donutbrowser.com>"]
|
||||
version = "0.2.2"
|
||||
edition = "2021"
|
||||
rust-version = "1.93.1"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.6", features = ["derive"] }
|
||||
@@ -22,22 +23,22 @@ reqwest = { version = "0.13", default-features = false, features = [
|
||||
"http2",
|
||||
] }
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
serde_json = "1.0.151"
|
||||
uuid = { version = "1.24", features = ["v4"] }
|
||||
zip = { version = "8", default-features = false, features = ["deflate", "zstd"] }
|
||||
serde_json = "1.0.152"
|
||||
uuid = { version = "1.28", features = ["v4"] }
|
||||
zip = { version = "9", default-features = false, features = ["deflate", "zstd"] }
|
||||
directories = "6"
|
||||
anyhow = "1.0.104"
|
||||
walkdir = "2.5"
|
||||
futures-util = "0.3.33"
|
||||
futures-util = "0.3.34"
|
||||
tempfile = "3.27"
|
||||
indicatif = "0.18.6"
|
||||
log = "0.4.33"
|
||||
log = "0.4.34"
|
||||
env_logger = "0.11.11"
|
||||
indicatif-log-bridge = "0.2.3"
|
||||
console = "0.16.4"
|
||||
console = "0.16.6"
|
||||
tar = "0.4.46"
|
||||
# sevenz-rust is unmaintained; sevenz-rust2 is the maintained fork.
|
||||
sevenz-rust2 = "0.21"
|
||||
sevenz-rust2 = "0.24"
|
||||
lzma-rs = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Banderole
|
||||
|
||||
Create cross-platform single-executables for Node.js projects. Windows is not supported.
|
||||
Create cross-platform single-executables for Node.js projects on Linux, macOS and Windows (x64 and arm64).
|
||||
|
||||
Banderole bundles your Node.js app, all dependencies, and a portable Node binary into a single native executable. On first launch, it unpacks to a cache directory for fast subsequent executions.
|
||||
|
||||
|
||||
+89
-39
@@ -42,8 +42,6 @@ const NODE_COMPRESSION_LEVEL: i64 = 12;
|
||||
/// automatically-generated name is used.
|
||||
/// * `custom_name` – optional custom name for the executable.
|
||||
/// * `no_compression` – disable compression for faster bundling (useful for testing).
|
||||
///
|
||||
/// The implementation uses a simpler, more reliable approach based on Playwright's bundling strategy.
|
||||
pub async fn bundle_project(
|
||||
project_path: PathBuf,
|
||||
output_path: Option<PathBuf>,
|
||||
@@ -165,14 +163,14 @@ pub async fn bundle_project(
|
||||
// high level there turns bundling a large project into minutes of compression. The
|
||||
// Node binary is a fixed ~105 MB that dominates the output size, so it is worth
|
||||
// compressing hard; a couple of seconds buys ~25% off the dominant term.
|
||||
let opts: zip::write::FileOptions<'static, ()> = if no_compression {
|
||||
let opts: zip::write::SimpleFileOptions = if no_compression {
|
||||
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored)
|
||||
} else {
|
||||
zip::write::FileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Zstd)
|
||||
.compression_level(Some(APP_COMPRESSION_LEVEL))
|
||||
};
|
||||
let node_opts: zip::write::FileOptions<'static, ()> = if no_compression {
|
||||
let node_opts: zip::write::SimpleFileOptions = if no_compression {
|
||||
opts
|
||||
} else {
|
||||
opts.compression_level(Some(NODE_COMPRESSION_LEVEL))
|
||||
@@ -272,7 +270,7 @@ fn count_files_in_dir(dir: &Path, exclude_node_modules: bool, follow_links: bool
|
||||
fn add_node_runtime_to_zip<W>(
|
||||
zip: &mut ZipWriter<W>,
|
||||
node_root: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
opts: zip::write::SimpleFileOptions,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -356,7 +354,7 @@ fn bundle_dependencies<W>(
|
||||
project_path: &Path,
|
||||
source_dir: &Path,
|
||||
_package_value: &Value,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
opts: zip::write::SimpleFileOptions,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -411,7 +409,7 @@ struct DependenciesResult {
|
||||
fn find_and_bundle_dependencies<W>(
|
||||
zip: &mut ZipWriter<W>,
|
||||
project_path: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
opts: zip::write::SimpleFileOptions,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<DependenciesResult>
|
||||
where
|
||||
@@ -429,7 +427,7 @@ where
|
||||
entries.flatten().any(|entry| {
|
||||
if entry.file_type().ok().is_some_and(|ft| ft.is_symlink()) {
|
||||
if let Ok(target) = fs::read_link(entry.path()) {
|
||||
let target_str = target.to_string_lossy();
|
||||
let target_str = target.to_string_lossy().replace('\\', "/");
|
||||
target_str.contains("/.pnpm/") && target_str.starts_with("../")
|
||||
} else {
|
||||
false
|
||||
@@ -590,7 +588,7 @@ fn detect_package_manager(node_modules_path: &Path, project_path: &Path) -> Pack
|
||||
for entry in entries.flatten() {
|
||||
if entry.file_type().ok().is_some_and(|ft| ft.is_symlink()) {
|
||||
if let Ok(target) = fs::read_link(entry.path()) {
|
||||
let target_str = target.to_string_lossy();
|
||||
let target_str = target.to_string_lossy().replace('\\', "/");
|
||||
if target_str.contains("/.pnpm/") {
|
||||
return PackageManager::Pnpm;
|
||||
}
|
||||
@@ -619,7 +617,7 @@ fn detect_package_manager(node_modules_path: &Path, project_path: &Path) -> Pack
|
||||
fn bundle_pnpm_dependencies<W>(
|
||||
zip: &mut ZipWriter<W>,
|
||||
project_path: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
opts: zip::write::SimpleFileOptions,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -711,7 +709,7 @@ where
|
||||
let file_path = node_modules_path.join(file_name);
|
||||
if file_path.exists() {
|
||||
let dest_path = Path::new("app/node_modules").join(file_name);
|
||||
zip.start_file(dest_path.to_string_lossy().as_ref(), opts)?;
|
||||
zip.start_file(zip_entry_name(&dest_path), opts)?;
|
||||
let data = fs::read(&file_path)?;
|
||||
zip.write_all(&data)?;
|
||||
}
|
||||
@@ -882,7 +880,7 @@ fn copy_pnpm_package_comprehensive<W>(
|
||||
node_modules_path: &Path,
|
||||
pnpm_dir: &Path,
|
||||
package_name: &str,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
opts: zip::write::SimpleFileOptions,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -951,7 +949,7 @@ fn bundle_node_modules_comprehensive<W>(
|
||||
zip: &mut ZipWriter<W>,
|
||||
node_modules_path: &Path,
|
||||
project_path: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
opts: zip::write::SimpleFileOptions,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -1058,7 +1056,7 @@ where
|
||||
let file_path = node_modules_path.join(file_name);
|
||||
if file_path.exists() {
|
||||
let dest_path = Path::new("app/node_modules").join(file_name);
|
||||
zip.start_file(dest_path.to_string_lossy().as_ref(), opts)?;
|
||||
zip.start_file(zip_entry_name(&dest_path), opts)?;
|
||||
let data = fs::read(&file_path)?;
|
||||
zip.write_all(&data)?;
|
||||
if let Some(pb) = progress {
|
||||
@@ -1076,7 +1074,7 @@ fn bundle_workspace_dependencies<W>(
|
||||
node_modules_path: &Path,
|
||||
_parent_path: &Path,
|
||||
project_path: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
opts: zip::write::SimpleFileOptions,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -1148,7 +1146,7 @@ where
|
||||
let file_path = node_modules_path.join(file_name);
|
||||
if file_path.exists() {
|
||||
let dest_path = Path::new("app/node_modules").join(file_name);
|
||||
zip.start_file(dest_path.to_string_lossy().as_ref(), opts)?;
|
||||
zip.start_file(zip_entry_name(&dest_path), opts)?;
|
||||
let data = fs::read(&file_path)?;
|
||||
zip.write_all(&data)?;
|
||||
if let Some(pb) = progress {
|
||||
@@ -1165,7 +1163,7 @@ fn bundle_pnpm_workspace_dependencies<W>(
|
||||
zip: &mut ZipWriter<W>,
|
||||
parent_path: &Path,
|
||||
project_path: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
opts: zip::write::SimpleFileOptions,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -1245,7 +1243,7 @@ where
|
||||
let file_path = parent_path.join("node_modules").join(file_name);
|
||||
if file_path.exists() {
|
||||
let dest_path = Path::new("app/node_modules").join(file_name);
|
||||
zip.start_file(dest_path.to_string_lossy().as_ref(), opts)?;
|
||||
zip.start_file(zip_entry_name(&dest_path), opts)?;
|
||||
let data = fs::read(&file_path)?;
|
||||
zip.write_all(&data)?;
|
||||
if let Some(pb) = progress {
|
||||
@@ -1480,12 +1478,72 @@ fn resolve_output_path(
|
||||
// Utility helpers
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Add directory to zip without following symlinks but preserving them
|
||||
/// Archive entry name for a relative path. Zip names always use `/`, whatever the host
|
||||
/// separator is, so a bundle made on Windows extracts the same way as one made elsewhere.
|
||||
fn zip_entry_name(path: &Path) -> String {
|
||||
path.components()
|
||||
.map(|c| c.as_os_str().to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
}
|
||||
|
||||
/// Add what a symlink points at under the link's own archive path.
|
||||
///
|
||||
/// The launcher extracts plain files only. If the link itself went into the archive, the
|
||||
/// extracted bundle would have a small text file with the target path where a module or a
|
||||
/// package directory should be. Links on Windows are often junctions, which this handles too.
|
||||
fn add_symlink_target_to_zip<W>(
|
||||
zip: &mut ZipWriter<W>,
|
||||
link: &Path,
|
||||
zip_path: &Path,
|
||||
opts: zip::write::SimpleFileOptions,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: Write + Read + std::io::Seek,
|
||||
{
|
||||
let Ok(meta) = fs::metadata(link) else {
|
||||
debug!("Skipping broken symlink {}", link.display());
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let add_file = |zip: &mut ZipWriter<W>, src: &Path, dest: &Path| -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
let opts = {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
opts.unix_permissions(fs::metadata(src)?.permissions().mode())
|
||||
};
|
||||
zip.start_file(zip_entry_name(dest), opts)?;
|
||||
let data = fs::read(src)
|
||||
.with_context(|| format!("Failed to read {} while zipping", src.display()))?;
|
||||
zip.write_all(&data)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if meta.is_file() {
|
||||
return add_file(zip, link, zip_path);
|
||||
}
|
||||
|
||||
// A link loop makes walkdir return an error for that entry; skip it, as a broken link.
|
||||
for entry in walkdir::WalkDir::new(link)
|
||||
.follow_links(true)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
if entry.file_type().is_file() {
|
||||
let rel = entry.path().strip_prefix(link).unwrap_or(entry.path());
|
||||
add_file(zip, entry.path(), &zip_path.join(rel))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add directory to zip. Symlinks are not followed during the walk; their targets are stored
|
||||
/// under the link's path instead.
|
||||
fn add_dir_to_zip_no_follow<W>(
|
||||
zip: &mut ZipWriter<W>,
|
||||
src_dir: &Path,
|
||||
dest_dir: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
opts: zip::write::SimpleFileOptions,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -1498,7 +1556,7 @@ where
|
||||
let zip_path = dest_dir.join(rel_path);
|
||||
|
||||
if entry.file_type().is_dir() {
|
||||
zip.add_directory(zip_path.to_string_lossy().as_ref(), opts)?;
|
||||
zip.add_directory(zip_entry_name(&zip_path), opts)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1521,14 +1579,10 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
zip.start_file(zip_path.to_string_lossy().as_ref(), file_opts)?;
|
||||
|
||||
if entry.file_type().is_symlink() {
|
||||
if let Ok(target) = fs::read_link(path) {
|
||||
let target_str = target.to_string_lossy();
|
||||
zip.write_all(target_str.as_bytes())?;
|
||||
}
|
||||
add_symlink_target_to_zip(zip, path, &zip_path, opts)?;
|
||||
} else {
|
||||
zip.start_file(zip_entry_name(&zip_path), file_opts)?;
|
||||
let data = fs::read(path).context("Failed to read file while zipping")?;
|
||||
zip.write_all(&data)?;
|
||||
}
|
||||
@@ -1544,7 +1598,7 @@ fn add_dir_to_zip_no_follow_skip_parents<W>(
|
||||
zip: &mut ZipWriter<W>,
|
||||
src_dir: &Path,
|
||||
dest_dir: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
opts: zip::write::SimpleFileOptions,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -1558,7 +1612,7 @@ where
|
||||
|
||||
if entry.file_type().is_dir() {
|
||||
if !rel_path.as_os_str().is_empty() {
|
||||
zip.add_directory(zip_path.to_string_lossy().as_ref(), opts)?;
|
||||
zip.add_directory(zip_entry_name(&zip_path), opts)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -1582,14 +1636,10 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
zip.start_file(zip_path.to_string_lossy().as_ref(), file_opts)?;
|
||||
|
||||
if entry.file_type().is_symlink() {
|
||||
if let Ok(target) = fs::read_link(path) {
|
||||
let target_str = target.to_string_lossy();
|
||||
zip.write_all(target_str.as_bytes())?;
|
||||
}
|
||||
add_symlink_target_to_zip(zip, path, &zip_path, opts)?;
|
||||
} else {
|
||||
zip.start_file(zip_entry_name(&zip_path), file_opts)?;
|
||||
let data = fs::read(path).context("Failed to read file while zipping")?;
|
||||
zip.write_all(&data)?;
|
||||
}
|
||||
@@ -1605,7 +1655,7 @@ fn add_dir_to_zip_excluding_node_modules<W>(
|
||||
zip: &mut ZipWriter<W>,
|
||||
src_dir: &Path,
|
||||
dest_dir: &Path,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
opts: zip::write::SimpleFileOptions,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -1622,7 +1672,7 @@ where
|
||||
}
|
||||
|
||||
if entry.file_type().is_dir() {
|
||||
zip.add_directory(zip_path.to_string_lossy().as_ref(), opts)?;
|
||||
zip.add_directory(zip_entry_name(&zip_path), opts)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1645,7 +1695,7 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
zip.start_file(zip_path.to_string_lossy().as_ref(), file_opts)?;
|
||||
zip.start_file(zip_entry_name(&zip_path), file_opts)?;
|
||||
let data = fs::read(path).context("Failed to read file while zipping")?;
|
||||
zip.write_all(&data)?;
|
||||
if let Some(pb) = progress {
|
||||
@@ -1660,7 +1710,7 @@ fn copy_workspace_package<W>(
|
||||
zip: &mut ZipWriter<W>,
|
||||
node_modules_path: &Path,
|
||||
package_name: &str,
|
||||
opts: zip::write::FileOptions<'static, ()>,
|
||||
opts: zip::write::SimpleFileOptions,
|
||||
progress: Option<&ProgressBar>,
|
||||
) -> Result<()>
|
||||
where
|
||||
|
||||
+12
-1
@@ -8,11 +8,22 @@ anyhow = "1.0"
|
||||
directories = "6"
|
||||
# Only the codecs the bundler can actually emit. The default feature set drags in bzip2,
|
||||
# liblzma, aes, zopfli, deflate64 and ppmd-rust, none of which are ever used.
|
||||
zip = { version = "8", default-features = false, features = ["deflate", "zstd"] }
|
||||
zip = { version = "9", default-features = false, features = ["deflate", "zstd"] }
|
||||
serde_json = "1.0"
|
||||
# fs2 is unmaintained (last release 2018); fs4 is the maintained fork.
|
||||
fs4 = { version = "1.1", default-features = false, features = ["sync"] }
|
||||
|
||||
# Already a dependency of fs4 on Windows, so the launcher's job object and console handler
|
||||
# add no extra crate to compile.
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_System_Console",
|
||||
"Win32_System_JobObjects",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
[build-dependencies]
|
||||
# No build dependencies needed - data is embedded at compile time
|
||||
|
||||
|
||||
+80
-11
@@ -70,7 +70,13 @@ fn main() -> Result<()> {
|
||||
if let Ok(main_script) = fs::read_to_string(&ready_path) {
|
||||
let main_script = main_script.trim().to_string();
|
||||
if !main_script.is_empty() {
|
||||
// Without exec() the launcher waits for Node, so it must release the lock first.
|
||||
// Otherwise every other launch would block until this app exits.
|
||||
#[cfg(windows)]
|
||||
FileExt::unlock(&lock_file).context("Failed to release extraction lock")?;
|
||||
run_app(&app_dir, &main_script, &args[1..])?;
|
||||
#[cfg(windows)]
|
||||
FileExt::lock(&lock_file).context("Failed to acquire extraction lock")?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,8 +147,9 @@ fn extract_application(app_dir: &Path) -> Result<()> {
|
||||
let entry = archive
|
||||
.by_index_raw(index)
|
||||
.context("Failed to read zip entry")?;
|
||||
let is_dir = entry.is_dir() || entry.name().ends_with('/');
|
||||
let Some(path) = safe_join(app_dir, entry.name()) else {
|
||||
let name = entry.name().context("Failed to read zip entry name")?;
|
||||
let is_dir = entry.is_dir() || name.ends_with('/');
|
||||
let Some(path) = safe_join(app_dir, &name) else {
|
||||
continue;
|
||||
};
|
||||
let size = entry.size();
|
||||
@@ -184,7 +191,7 @@ fn extract_application(app_dir: &Path) -> Result<()> {
|
||||
// static split of the work list hands it to one worker that is still writing long after
|
||||
// the rest have finished. Longest-job-first plus a shared cursor keeps every core busy to
|
||||
// the end.
|
||||
files.sort_unstable_by(|a, b| b.size.cmp(&a.size));
|
||||
files.sort_unstable_by_key(|f| std::cmp::Reverse(f.size));
|
||||
|
||||
let threads = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
@@ -328,7 +335,9 @@ fn run_app(app_dir: &Path, main_script: &str, args: &[std::ffi::OsString]) -> Re
|
||||
command.env("NODE_COMPILE_CACHE", app_dir.join(".v8cache"));
|
||||
}
|
||||
|
||||
// The early return is needed on Unix only because the Windows block follows it.
|
||||
#[cfg(unix)]
|
||||
#[allow(clippy::needless_return)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
// Replaces this process image with Node: no fork, no resident parent holding the
|
||||
@@ -348,16 +357,22 @@ fn run_app(app_dir: &Path, main_script: &str, args: &[std::ffi::OsString]) -> Re
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::process::Stdio;
|
||||
// Windows has no exec(), so the launcher stays as Node's parent. It must not die
|
||||
// before Node on Ctrl+C (the shell would get its prompt back while Node still
|
||||
// writes to the console), and Node must not outlive a launcher that is killed.
|
||||
windows_supervisor::ignore_console_interrupts();
|
||||
|
||||
let mut last_err: Option<std::io::Error> = None;
|
||||
for attempt in 1..=8u32 {
|
||||
match command
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.status()
|
||||
{
|
||||
Ok(status) => std::process::exit(status.code().unwrap_or(1)),
|
||||
match command.spawn() {
|
||||
Ok(mut child) => {
|
||||
windows_supervisor::kill_on_launcher_exit(&child);
|
||||
let code = match child.wait() {
|
||||
Ok(status) => status.code().unwrap_or(1),
|
||||
Err(_) => 1,
|
||||
};
|
||||
std::process::exit(code);
|
||||
}
|
||||
Err(e) => {
|
||||
last_err = Some(e);
|
||||
std::thread::sleep(std::time::Duration::from_millis(50 * attempt as u64));
|
||||
@@ -377,6 +392,60 @@ fn run_app(app_dir: &Path, main_script: &str, args: &[std::ffi::OsString]) -> Re
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows_supervisor {
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
use windows_sys::core::BOOL;
|
||||
use windows_sys::Win32::System::Console::{
|
||||
SetConsoleCtrlHandler, CTRL_BREAK_EVENT, CTRL_C_EVENT,
|
||||
};
|
||||
use windows_sys::Win32::System::JobObjects::{
|
||||
AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
|
||||
SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
|
||||
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK,
|
||||
};
|
||||
|
||||
unsafe extern "system" fn on_console_event(event: u32) -> BOOL {
|
||||
// Ctrl+C and Ctrl+Break go to every process on the console, so Node gets them too
|
||||
// and decides how to exit. The launcher only waits and forwards the exit code.
|
||||
(event == CTRL_C_EVENT || event == CTRL_BREAK_EVENT) as BOOL
|
||||
}
|
||||
|
||||
pub fn ignore_console_interrupts() {
|
||||
// A handler routine, unlike `SetConsoleCtrlHandler(NULL, TRUE)`, is not inherited,
|
||||
// so Node keeps the default Ctrl+C behaviour.
|
||||
unsafe {
|
||||
SetConsoleCtrlHandler(Some(on_console_event), 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Put Node in a job that is closed, and so terminated, when the launcher exits for any
|
||||
/// reason. Processes that Node starts are not added to the job, so detached children
|
||||
/// still outlive the app. Failures are ignored: the app then runs as before.
|
||||
pub fn kill_on_launcher_exit(child: &std::process::Child) {
|
||||
unsafe {
|
||||
let job = CreateJobObjectW(std::ptr::null(), std::ptr::null());
|
||||
if job.is_null() {
|
||||
return;
|
||||
}
|
||||
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
|
||||
info.BasicLimitInformation.LimitFlags =
|
||||
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
|
||||
let configured = SetInformationJobObject(
|
||||
job,
|
||||
JobObjectExtendedLimitInformation,
|
||||
&info as *const _ as *const _,
|
||||
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
|
||||
);
|
||||
if configured != 0 {
|
||||
AssignProcessToJobObject(job, child.as_raw_handle() as _);
|
||||
}
|
||||
// The job handle stays open on purpose. The system closes it when the launcher
|
||||
// exits, and that terminates Node.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_main_script(app_path: &Path) -> Result<String> {
|
||||
let package_json_path = app_path.join("package.json");
|
||||
|
||||
|
||||
+37
-146
@@ -7,6 +7,20 @@ use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Build a command for a Node package manager (`npm`, `pnpm`, ...).
|
||||
///
|
||||
/// On Windows these are `.cmd` shims, which `Command::new` does not find by bare name, so
|
||||
/// they go through `cmd /C`.
|
||||
pub fn package_manager_command(program: &str) -> Command {
|
||||
if cfg!(windows) {
|
||||
let mut cmd = Command::new("cmd");
|
||||
cmd.args(["/C", program]);
|
||||
cmd
|
||||
} else {
|
||||
Command::new(program)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents different project types for testing
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProjectType {
|
||||
@@ -146,7 +160,7 @@ impl TestProjectManager {
|
||||
|
||||
/// Install dependencies using npm
|
||||
pub fn install_dependencies(&self) -> Result<()> {
|
||||
let npm_install = Command::new("npm")
|
||||
let npm_install = package_manager_command("npm")
|
||||
.args(["install"])
|
||||
.current_dir(&self.project_path)
|
||||
.output()?;
|
||||
@@ -164,7 +178,7 @@ impl TestProjectManager {
|
||||
/// Install dependencies using pnpm
|
||||
pub fn install_pnpm_dependencies(&self) -> Result<()> {
|
||||
// First try pnpm
|
||||
let pnpm_install = Command::new("pnpm")
|
||||
let pnpm_install = package_manager_command("pnpm")
|
||||
.args(["install"])
|
||||
.current_dir(self.workspace_root.as_ref().unwrap_or(&self.project_path))
|
||||
.output();
|
||||
@@ -188,7 +202,7 @@ impl TestProjectManager {
|
||||
/// Install dependencies in workspace root
|
||||
pub fn install_workspace_dependencies(&self) -> Result<()> {
|
||||
if let Some(workspace_root) = &self.workspace_root {
|
||||
let npm_install = Command::new("npm")
|
||||
let npm_install = package_manager_command("npm")
|
||||
.args(["install"])
|
||||
.current_dir(workspace_root)
|
||||
.output()?;
|
||||
@@ -494,27 +508,8 @@ pub struct BundlerTestHelper;
|
||||
impl BundlerTestHelper {
|
||||
/// Get the path to the banderole binary
|
||||
pub fn get_bundler_path() -> Result<PathBuf> {
|
||||
let target_dir = std::env::current_dir()?.join("target");
|
||||
let bundler_path = if cfg!(windows) {
|
||||
target_dir.join("debug/banderole.exe")
|
||||
} else {
|
||||
target_dir.join("debug/banderole")
|
||||
};
|
||||
|
||||
if !bundler_path.exists() {
|
||||
// Build the bundler if it doesn't exist
|
||||
println!("Building banderole...");
|
||||
let output = Command::new("cargo").args(["build"]).output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!(
|
||||
"Failed to build banderole: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bundler_path)
|
||||
// Cargo builds the binary before it runs integration tests and reports its path.
|
||||
Ok(PathBuf::from(env!("CARGO_BIN_EXE_banderole")))
|
||||
}
|
||||
|
||||
/// Bundle a project and return the path to the created executable
|
||||
@@ -697,123 +692,26 @@ impl BundlerTestHelper {
|
||||
fs::set_permissions(executable_path, perms)?;
|
||||
}
|
||||
|
||||
// Build command to run the executable.
|
||||
#[cfg(windows)]
|
||||
let (exec_to_run, work_dir, _run_guard) = {
|
||||
// Copy to a unique name in the same directory as the original to avoid policy issues with %TEMP%
|
||||
let parent = executable_path.parent().ok_or_else(|| {
|
||||
anyhow::anyhow!("Executable has no parent: {}", executable_path.display())
|
||||
})?;
|
||||
let run_dir = TempDir::new_in(parent).unwrap_or_else(|_| TempDir::new().unwrap());
|
||||
let mut base = executable_path
|
||||
.file_name()
|
||||
.map(|s| s.to_os_string())
|
||||
.unwrap_or_else(|| "app.exe".into());
|
||||
if Path::new(&base).extension().is_none() {
|
||||
base.push(".exe");
|
||||
}
|
||||
let candidate = run_dir.path().join(&base);
|
||||
std::fs::copy(executable_path, &candidate).with_context(|| {
|
||||
format!(
|
||||
"Failed to copy executable to run dir: {} -> {}",
|
||||
executable_path.display(),
|
||||
candidate.display()
|
||||
)
|
||||
})?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
if !candidate.exists() {
|
||||
anyhow::bail!(
|
||||
"Run executable not found after copy: {}",
|
||||
candidate.display()
|
||||
);
|
||||
}
|
||||
(candidate, parent.to_path_buf(), run_dir)
|
||||
};
|
||||
let work_dir = executable_path.parent().ok_or_else(|| {
|
||||
anyhow::anyhow!("Executable has no parent: {}", executable_path.display())
|
||||
})?;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let (exec_to_run, work_dir) = (
|
||||
executable_path.to_path_buf(),
|
||||
executable_path.parent().unwrap().to_path_buf(),
|
||||
println!(
|
||||
"Executing: {} with args: {:?}",
|
||||
executable_path.display(),
|
||||
args
|
||||
);
|
||||
|
||||
println!("Executing: {} with args: {:?}", exec_to_run.display(), args);
|
||||
let mut cmd = Command::new(executable_path);
|
||||
cmd.args(args).current_dir(work_dir);
|
||||
for (key, value) in env_vars {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
|
||||
// Build a verbatim Windows path to avoid MAX_PATH and normalization issues
|
||||
#[cfg(windows)]
|
||||
let exec_for_spawn = {
|
||||
use std::ffi::OsString;
|
||||
let abs = exec_to_run
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| exec_to_run.clone());
|
||||
let mut s: OsString = OsString::from(r"\\?\");
|
||||
s.push(&abs);
|
||||
s
|
||||
};
|
||||
#[cfg(not(windows))]
|
||||
let exec_for_spawn = exec_to_run.as_os_str().to_os_string();
|
||||
|
||||
// First try direct spawn
|
||||
let direct = {
|
||||
let mut cmd = Command::new(&exec_for_spawn);
|
||||
cmd.args(args);
|
||||
for (key, value) in env_vars {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
cmd.current_dir(&work_dir).output()
|
||||
};
|
||||
|
||||
// If NotFound on Windows, retry using the copied executable directly with verbatim prefix; else cmd /C
|
||||
#[cfg(windows)]
|
||||
let output = match direct {
|
||||
Ok(o) => Ok(o),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
use std::ffi::OsString;
|
||||
let abs = exec_to_run
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| exec_to_run.clone());
|
||||
let mut verbatim: OsString = OsString::from(r"\\?\");
|
||||
verbatim.push(&abs);
|
||||
let mut cmd = Command::new(&verbatim);
|
||||
cmd.args(args).current_dir(&work_dir);
|
||||
for (key, value) in env_vars {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
match cmd.output() {
|
||||
Ok(o2) => Ok(o2),
|
||||
Err(e2) if e2.kind() == std::io::ErrorKind::NotFound => {
|
||||
// Fallback to cmd /C with quoting
|
||||
fn quote(s: &str) -> String {
|
||||
let mut out = String::from("\"");
|
||||
out.push_str(&s.replace('"', "\\\""));
|
||||
out.push('"');
|
||||
out
|
||||
}
|
||||
let exe_str = exec_to_run.display().to_string();
|
||||
let mut cmdline = quote(&exe_str);
|
||||
for a in args {
|
||||
cmdline.push(' ');
|
||||
cmdline.push_str("e(a));
|
||||
}
|
||||
let mut c2 = Command::new("cmd");
|
||||
c2.args(["/C", &cmdline]).current_dir(&work_dir);
|
||||
for (key, value) in env_vars {
|
||||
c2.env(key, value);
|
||||
}
|
||||
c2.output()
|
||||
}
|
||||
Err(e2) => Err(e2),
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let output = direct;
|
||||
|
||||
let output = output.with_context(|| {
|
||||
let output = cmd.output().with_context(|| {
|
||||
format!(
|
||||
"Failed to execute command: {}\nArgs: {:?}\nEnv vars: {:?}\nWorking directory: {:?}",
|
||||
exec_to_run.display(),
|
||||
executable_path.display(),
|
||||
args,
|
||||
env_vars,
|
||||
work_dir
|
||||
@@ -864,17 +762,10 @@ pub struct TestCacheManager;
|
||||
impl TestCacheManager {
|
||||
/// The directory the launcher extracts bundles into, matching `directories::BaseDirs`.
|
||||
pub fn application_cache_dir() -> PathBuf {
|
||||
if cfg!(windows) {
|
||||
std::env::var_os("LOCALAPPDATA")
|
||||
.map(|d| PathBuf::from(d).join("banderole"))
|
||||
.unwrap_or_else(|| PathBuf::from("banderole-cache"))
|
||||
} else if let Some(xdg_cache) = std::env::var_os("XDG_CACHE_HOME") {
|
||||
PathBuf::from(xdg_cache).join("banderole")
|
||||
} else if let Some(home) = std::env::var_os("HOME") {
|
||||
PathBuf::from(home).join(".cache").join("banderole")
|
||||
} else {
|
||||
PathBuf::from("/tmp").join("banderole-cache")
|
||||
}
|
||||
directories::BaseDirs::new()
|
||||
.expect("Failed to determine base directories")
|
||||
.cache_dir()
|
||||
.join("banderole")
|
||||
}
|
||||
|
||||
/// Clear application cache for testing
|
||||
|
||||
+18
-122
@@ -77,28 +77,12 @@ process.exit(0);"#;
|
||||
);
|
||||
}
|
||||
|
||||
// Build banderole if not already built
|
||||
println!("Building banderole...");
|
||||
let target_dir = std::env::current_dir()?.join("target");
|
||||
let banderole_path = target_dir.join("debug/banderole");
|
||||
|
||||
if !banderole_path.exists() {
|
||||
let output = Command::new("cargo")
|
||||
.args(["build"])
|
||||
.output()
|
||||
.expect("Failed to build banderole");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Failed to build banderole: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let banderole_path = env!("CARGO_BIN_EXE_banderole");
|
||||
|
||||
// Bundle the test app)
|
||||
println!("Bundling test app...");
|
||||
|
||||
let mut bundle_cmd = Command::new(&banderole_path);
|
||||
let mut bundle_cmd = Command::new(banderole_path);
|
||||
bundle_cmd
|
||||
.args([
|
||||
"bundle",
|
||||
@@ -233,26 +217,11 @@ process.exit(0);"#;
|
||||
std::fs::write(test_app_path.join("index.js"), index_js).unwrap();
|
||||
std::fs::write(test_app_path.join(".nvmrc"), nvmrc).unwrap();
|
||||
|
||||
// Build banderole
|
||||
println!("Building banderole for Node version test...");
|
||||
let output = Command::new("cargo")
|
||||
.args(["build", "--release"])
|
||||
.output()
|
||||
.expect("Failed to build banderole");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Failed to build banderole: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let banderole_path = std::env::current_dir()
|
||||
.unwrap()
|
||||
.join("target/release/banderole");
|
||||
let banderole_path = env!("CARGO_BIN_EXE_banderole");
|
||||
|
||||
// Bundle the test app (keep compression for this test to verify it works)
|
||||
println!("Bundling test app with .nvmrc...");
|
||||
let mut bundle_cmd = Command::new(&banderole_path);
|
||||
let mut bundle_cmd = Command::new(banderole_path);
|
||||
bundle_cmd
|
||||
.args(["bundle", test_app_path.to_str().unwrap()])
|
||||
.current_dir(temp_dir.path());
|
||||
@@ -368,27 +337,12 @@ async fn test_output_path_collision_handling() -> Result<(), Box<dyn std::error:
|
||||
// Create a directory with the same name as the app to cause collision
|
||||
std::fs::create_dir_all(temp_dir.path().join("collision-test-app"))?;
|
||||
|
||||
// Build banderole
|
||||
let target_dir = std::env::current_dir()?.join("target");
|
||||
let banderole_path = target_dir.join("debug/banderole");
|
||||
|
||||
if !banderole_path.exists() {
|
||||
let output = Command::new("cargo")
|
||||
.args(["build"])
|
||||
.output()
|
||||
.expect("Failed to build banderole");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Failed to build banderole: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let banderole_path = env!("CARGO_BIN_EXE_banderole");
|
||||
|
||||
// Bundle the test app)
|
||||
println!("Testing output path collision handling...");
|
||||
|
||||
let mut bundle_cmd = Command::new(&banderole_path);
|
||||
let mut bundle_cmd = Command::new(banderole_path);
|
||||
bundle_cmd
|
||||
.args([
|
||||
"bundle",
|
||||
@@ -506,27 +460,12 @@ try {
|
||||
std::fs::write(test_app_path.join("dist/index.js"), dist_index_js)?;
|
||||
std::fs::write(test_app_path.join("dist/marker.js"), dist_marker)?;
|
||||
|
||||
// Build banderole
|
||||
let target_dir = std::env::current_dir()?.join("target");
|
||||
let banderole_path = target_dir.join("debug/banderole");
|
||||
|
||||
if !banderole_path.exists() {
|
||||
let output = Command::new("cargo")
|
||||
.args(["build"])
|
||||
.output()
|
||||
.expect("Failed to build banderole");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Failed to build banderole: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let banderole_path = env!("CARGO_BIN_EXE_banderole");
|
||||
|
||||
// Bundle the TypeScript project)
|
||||
println!("Testing TypeScript project bundling...");
|
||||
|
||||
let mut bundle_cmd = Command::new(&banderole_path);
|
||||
let mut bundle_cmd = Command::new(banderole_path);
|
||||
bundle_cmd
|
||||
.args([
|
||||
"bundle",
|
||||
@@ -654,27 +593,12 @@ try {
|
||||
std::fs::write(test_app_path.join("build/index.js"), build_index_js)?;
|
||||
std::fs::write(test_app_path.join("build/marker.js"), build_marker)?;
|
||||
|
||||
// Build banderole
|
||||
let target_dir = std::env::current_dir()?.join("target");
|
||||
let banderole_path = target_dir.join("debug/banderole");
|
||||
|
||||
if !banderole_path.exists() {
|
||||
let output = Command::new("cargo")
|
||||
.args(["build"])
|
||||
.output()
|
||||
.expect("Failed to build banderole");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Failed to build banderole: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let banderole_path = env!("CARGO_BIN_EXE_banderole");
|
||||
|
||||
// Bundle the project)
|
||||
println!("Testing TypeScript project with custom outDir...");
|
||||
|
||||
let mut bundle_cmd = Command::new(&banderole_path);
|
||||
let mut bundle_cmd = Command::new(banderole_path);
|
||||
bundle_cmd
|
||||
.args([
|
||||
"bundle",
|
||||
@@ -801,27 +725,12 @@ try {
|
||||
std::fs::write(test_app_path.join("lib/index.js"), lib_index_js)?;
|
||||
std::fs::write(test_app_path.join("lib/marker.js"), lib_marker)?;
|
||||
|
||||
// Build banderole
|
||||
let target_dir = std::env::current_dir()?.join("target");
|
||||
let banderole_path = target_dir.join("debug/banderole");
|
||||
|
||||
if !banderole_path.exists() {
|
||||
let output = Command::new("cargo")
|
||||
.args(["build"])
|
||||
.output()
|
||||
.expect("Failed to build banderole");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Failed to build banderole: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let banderole_path = env!("CARGO_BIN_EXE_banderole");
|
||||
|
||||
// Bundle the project)
|
||||
println!("Testing TypeScript project with extends...");
|
||||
|
||||
let mut bundle_cmd = Command::new(&banderole_path);
|
||||
let mut bundle_cmd = Command::new(banderole_path);
|
||||
bundle_cmd
|
||||
.args([
|
||||
"bundle",
|
||||
@@ -1035,27 +944,12 @@ packages:
|
||||
}
|
||||
}
|
||||
|
||||
// Build banderole
|
||||
let target_dir = std::env::current_dir()?.join("target");
|
||||
let banderole_path = target_dir.join("debug/banderole");
|
||||
|
||||
if !banderole_path.exists() {
|
||||
let output = Command::new("cargo")
|
||||
.args(["build"])
|
||||
.output()
|
||||
.expect("Failed to build banderole");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"Failed to build banderole: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let banderole_path = env!("CARGO_BIN_EXE_banderole");
|
||||
|
||||
// Bundle the pnpm project)
|
||||
println!("Testing pnpm dependency bundling...");
|
||||
|
||||
let mut bundle_cmd = Command::new(&banderole_path);
|
||||
let mut bundle_cmd = Command::new(banderole_path);
|
||||
bundle_cmd
|
||||
.args([
|
||||
"bundle",
|
||||
@@ -1235,8 +1129,10 @@ async fn test_bundle_simple_project() {
|
||||
String::from_utf8_lossy(&bundle_output.stderr)
|
||||
);
|
||||
|
||||
// Test that the bundle exists
|
||||
let bundle_path = temp_dir.path().join("test-bundle");
|
||||
// Test that the bundle exists (the bundler adds `.exe` on Windows)
|
||||
let bundle_path = temp_dir
|
||||
.path()
|
||||
.join(format!("test-bundle{}", std::env::consts::EXE_SUFFIX));
|
||||
assert!(bundle_path.exists(), "Bundle file not created");
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use common::{BundlerTestHelper, TestCacheManager};
|
||||
use serial_test::serial;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Writes a project whose entrypoint can exit with a chosen code or idle forever.
|
||||
@@ -103,7 +102,7 @@ async fn test_launcher_execs_into_node() -> Result<(), Box<dyn std::error::Error
|
||||
#[serial]
|
||||
async fn test_no_launcher_process_remains_resident() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use std::io::Read;
|
||||
use std::process::Stdio;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let app = write_probe_project(temp_dir.path())?;
|
||||
@@ -216,3 +215,54 @@ async fn test_payload_contains_only_node_executable() -> Result<(), Box<dyn std:
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Windows has no exec(), so the launcher stays as Node's parent. When the launcher is
|
||||
/// killed, Node must stop too, or it keeps the extracted files in use and runs unattended.
|
||||
#[cfg(windows)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn test_node_stops_when_launcher_is_killed() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use std::io::Read;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
let app = write_probe_project(temp_dir.path())?;
|
||||
let out_dir = temp_dir.path().join("out");
|
||||
fs::create_dir_all(&out_dir)?;
|
||||
|
||||
TestCacheManager::clear_application_cache()?;
|
||||
let exe = BundlerTestHelper::bundle_project(&app, &out_dir, Some("probe-app"))?;
|
||||
BundlerTestHelper::run_executable(&exe, &[], &[])?;
|
||||
|
||||
let mut child = Command::new(&exe)
|
||||
.arg("idle")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
let mut stdout = child.stdout.take().expect("piped stdout");
|
||||
let mut buf = [0u8; 32];
|
||||
let n = stdout.read(&mut buf)?;
|
||||
assert!(
|
||||
String::from_utf8_lossy(&buf[..n]).contains("ready"),
|
||||
"idle app never became ready"
|
||||
);
|
||||
|
||||
// Node holds the write end of the pipe too, so the read only ends when Node has exited.
|
||||
child.kill()?;
|
||||
child.wait()?;
|
||||
let (tx, rx) = mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let mut rest = Vec::new();
|
||||
let _ = stdout.read_to_end(&mut rest);
|
||||
let _ = tx.send(());
|
||||
});
|
||||
assert!(
|
||||
rx.recv_timeout(Duration::from_secs(15)).is_ok(),
|
||||
"Node kept running after the launcher was killed"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -103,26 +103,7 @@ fn prepare_node_modules_for_spec(app_dir: &Path, local_pkg_dir: &Path, mode: &st
|
||||
}
|
||||
|
||||
fn bundle_app(app_dir: &Path, out_dir: &Path, name: &str) -> PathBuf {
|
||||
// Build banderole if needed
|
||||
let bundler = {
|
||||
let target_dir = std::env::current_dir().unwrap().join("target");
|
||||
let path = if cfg!(windows) {
|
||||
target_dir.join("debug/banderole.exe")
|
||||
} else {
|
||||
target_dir.join("debug/banderole")
|
||||
};
|
||||
if !path.exists() {
|
||||
let out = Command::new("cargo").args(["build"]).output().unwrap();
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"Failed to build banderole: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
path
|
||||
};
|
||||
|
||||
let output = Command::new(&bundler)
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_banderole"))
|
||||
.args([
|
||||
"bundle",
|
||||
app_dir.to_str().unwrap(),
|
||||
|
||||
Reference in new issue
Block a user