mirror of
https://github.com/zhom/banderole.git
synced 2026-04-29 07:15:58 +02:00
220 lines
6.6 KiB
Rust
220 lines
6.6 KiB
Rust
use serial_test::serial;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
use tempfile::TempDir;
|
|
|
|
fn write_test_local_dep_package(pkg_dir: &Path) {
|
|
fs::create_dir_all(pkg_dir).unwrap();
|
|
let pkg_json = r#"{
|
|
"name": "test_local_dep",
|
|
"version": "1.0.0",
|
|
"main": "index.js"
|
|
}"#;
|
|
let index_js = r#"module.exports = {
|
|
greet() { return "test_local_dep says hi"; }
|
|
};"#;
|
|
fs::write(pkg_dir.join("package.json"), pkg_json).unwrap();
|
|
fs::write(pkg_dir.join("index.js"), index_js).unwrap();
|
|
}
|
|
|
|
fn write_app_with_dep(app_dir: &Path, dep_spec: &str) {
|
|
fs::create_dir_all(app_dir).unwrap();
|
|
let package_json = format!(
|
|
r#"{{
|
|
"name": "app-with-local-link",
|
|
"version": "1.0.0",
|
|
"main": "index.js",
|
|
"dependencies": {{
|
|
"test_local_dep": "{dep_spec}"
|
|
}}
|
|
}}"#
|
|
);
|
|
let index_js = r#"const test_local_dep = require('test_local_dep');
|
|
console.log('Hello from app');
|
|
console.log('test_local_dep:', test_local_dep.greet());
|
|
process.exit(0);"#;
|
|
fs::write(app_dir.join("package.json"), package_json).unwrap();
|
|
fs::write(app_dir.join("index.js"), index_js).unwrap();
|
|
}
|
|
|
|
fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
|
|
if dst.exists() {
|
|
let _ = fs::remove_dir_all(dst);
|
|
}
|
|
fs::create_dir_all(dst)?;
|
|
for entry in walkdir::WalkDir::new(src) {
|
|
let entry = entry.unwrap();
|
|
let path = entry.path();
|
|
let rel = path.strip_prefix(src).unwrap();
|
|
let target = dst.join(rel);
|
|
if entry.file_type().is_dir() {
|
|
fs::create_dir_all(&target)?;
|
|
} else if entry.file_type().is_file() {
|
|
if let Some(parent) = target.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
fs::copy(path, &target)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn make_symlink(target: &Path, link: &Path) -> std::io::Result<()> {
|
|
use std::os::unix::fs as unix_fs;
|
|
if link.exists() {
|
|
let _ = fs::remove_file(link);
|
|
let _ = fs::remove_dir_all(link);
|
|
}
|
|
if let Some(parent) = link.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
unix_fs::symlink(target, link)
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn make_symlink(target: &Path, link: &Path) -> std::io::Result<()> {
|
|
// Best-effort: try directory junction; fall back to copy if it fails
|
|
if link.exists() {
|
|
let _ = fs::remove_file(link);
|
|
let _ = fs::remove_dir_all(link);
|
|
}
|
|
if let Some(parent) = link.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
std::os::windows::fs::symlink_dir(target, link).or_else(|_| copy_dir_recursive(target, link))
|
|
}
|
|
|
|
fn prepare_node_modules_for_spec(app_dir: &Path, local_pkg_dir: &Path, mode: &str) {
|
|
let nm_test_local_dep = app_dir.join("node_modules").join("test_local_dep");
|
|
match mode {
|
|
"copy" => {
|
|
copy_dir_recursive(local_pkg_dir, &nm_test_local_dep).unwrap();
|
|
}
|
|
"symlink" => {
|
|
make_symlink(local_pkg_dir, &nm_test_local_dep).unwrap_or_else(|_| {
|
|
// Fallback to copy if symlink not permitted
|
|
copy_dir_recursive(local_pkg_dir, &nm_test_local_dep).unwrap();
|
|
});
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
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)
|
|
.args([
|
|
"bundle",
|
|
app_dir.to_str().unwrap(),
|
|
"--no-compression",
|
|
"--name",
|
|
name,
|
|
])
|
|
.current_dir(out_dir)
|
|
.output()
|
|
.unwrap();
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"Bundle failed: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
|
|
let exe = out_dir.join(if cfg!(windows) {
|
|
format!("{name}.exe")
|
|
} else {
|
|
name.to_string()
|
|
});
|
|
if exe.exists() && exe.is_file() {
|
|
exe
|
|
} else {
|
|
out_dir.join(if cfg!(windows) {
|
|
format!("{name}-bundle.exe")
|
|
} else {
|
|
format!("{name}-bundle")
|
|
})
|
|
}
|
|
}
|
|
|
|
fn copy_exe_to_fresh_dir(exe: &Path) -> PathBuf {
|
|
let run_dir = TempDir::new().unwrap();
|
|
let dst = run_dir.path().join(exe.file_name().unwrap_or_default());
|
|
fs::copy(exe, &dst).unwrap();
|
|
// Keep dir alive by leaking TempDir, test process will clean temp space later
|
|
Box::leak(Box::new(run_dir));
|
|
dst
|
|
}
|
|
|
|
fn run_and_assert(exe: &Path) {
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let mut perms = fs::metadata(exe).unwrap().permissions();
|
|
perms.set_mode(0o755);
|
|
fs::set_permissions(exe, perms).unwrap();
|
|
}
|
|
let out = Command::new(exe).output().unwrap();
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
assert!(
|
|
out.status.success(),
|
|
"Executable failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
assert!(stdout.contains("Hello from app"));
|
|
assert!(stdout.contains("test_local_dep: test_local_dep says hi"));
|
|
}
|
|
|
|
fn build_and_verify_portable(dep_spec: &str, mode: &str, name: &str) {
|
|
// Each case uses its own workspace
|
|
let root_temp = TempDir::new().unwrap();
|
|
let root = root_temp.path().to_path_buf();
|
|
|
|
let test_local_dep_dir = root.join("test_local_dep");
|
|
write_test_local_dep_package(&test_local_dep_dir);
|
|
|
|
let app = root.join("app");
|
|
write_app_with_dep(&app, dep_spec);
|
|
prepare_node_modules_for_spec(&app, &test_local_dep_dir, mode);
|
|
|
|
let exe = bundle_app(&app, &root, name);
|
|
let portable_exe = copy_exe_to_fresh_dir(&exe);
|
|
|
|
// Remove original workspace entirely to ensure we don't accidentally resolve files from it
|
|
let _ = fs::remove_dir_all(&root);
|
|
|
|
run_and_assert(&portable_exe);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
#[serial]
|
|
async fn test_local_link_variants() {
|
|
// 1) file:../test_local_dep
|
|
build_and_verify_portable("file:../test_local_dep", "copy", "app-file");
|
|
|
|
// 2) link:../test_local_dep (prefer symlink; fallback to copy if not permitted)
|
|
build_and_verify_portable("link:../test_local_dep", "symlink", "app-link");
|
|
|
|
// 3) ../test_local_dep (bare relative)
|
|
build_and_verify_portable("../test_local_dep", "symlink", "app-bare");
|
|
}
|