use std::path::{Path, PathBuf};
use std::process::Command;
use super::support::{portable_release_target, FakeReleaseServer, TempWorkspace};
pub(super) struct RealUseRelease {
pub(super) server: FakeReleaseServer,
pub(super) version: String,
}
pub(super) fn start(workspace: &TempWorkspace) -> RealUseRelease {
let binary = required_path("A3S_USE_E2E_BIN");
let source_root = required_path("A3S_USE_E2E_SOURCE_ROOT");
let browser_driver = binary
.parent()
.expect("real Use binary must have a parent directory")
.join("a3s-use-browser-driver");
assert!(
browser_driver.is_file(),
"real Browser driver is missing at {}",
browser_driver.display()
);
let version = use_version(&binary);
let target = portable_release_target().expect("test host must support a portable Use release");
let package_name = format!("a3s-use-{version}-{target}");
let release_root = workspace.path("real-release");
let package_root = release_root.join(&package_name);
std::fs::create_dir_all(&package_root).expect("create real Use package root");
copy_executable(&binary, &package_root.join("a3s-use"));
copy_executable(
&browser_driver,
&package_root.join("a3s-use-browser-driver"),
);
package_ocr_models(&binary, &package_root);
for (source, destination) in [
("crates/browser-driver/skills", "skills"),
("crates/browser-driver/skill-data", "skill-data"),
("crates/office/skills", "office-skills"),
("crates/ocr/skills", "ocr-skills"),
("crates/browser-driver/dashboard/out", "dashboard"),
] {
copy_tree(&source_root.join(source), &package_root.join(destination));
}
for source in ["LICENSE", "README.md", "THIRD_PARTY_NOTICES.md"] {
std::fs::copy(source_root.join(source), package_root.join(source))
.unwrap_or_else(|error| panic!("failed to package {source}: {error}"));
}
for source in ["LICENSE-APACHE-2.0", "UPSTREAM.md"] {
std::fs::copy(
source_root.join("crates/browser-driver").join(source),
package_root.join(source),
)
.unwrap_or_else(|error| panic!("failed to package {source}: {error}"));
}
let archive_name = format!("{package_name}.tar.gz");
let archive_path = workspace.path(&archive_name);
let status = Command::new("tar")
.arg("czf")
.arg(&archive_path)
.arg("-C")
.arg(&release_root)
.arg(&package_name)
.status()
.expect("create real Use release archive");
assert!(
status.success(),
"failed to create real Use release archive"
);
let archive = std::fs::read(archive_path).expect("read real Use release archive");
RealUseRelease {
server: FakeReleaseServer::start("Use", &version, &archive_name, archive),
version,
}
}
fn package_ocr_models(binary: &Path, package_root: &Path) {
let model_root = package_root.join("ocr-models");
let output = Command::new(binary)
.args(["component", "install", "ocr", "--force", "--json"])
.env("A3S_USE_OCR_HOME", &model_root)
.env_remove("A3S_OCR_MODEL_DIR")
.output()
.unwrap_or_else(|error| {
panic!(
"failed to package PP-OCRv6 models with {}: {error}",
binary.display()
)
});
assert!(
output.status.success(),
"failed to package PP-OCRv6 models: stdout={}\nstderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let status: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("decode PP-OCRv6 install response");
assert_eq!(status["ok"], true);
assert_eq!(status["data"]["runtime"]["model"], "PP-OCRv6_small");
let lock = model_root.join(".install.lock");
if lock.exists() {
std::fs::remove_file(&lock)
.unwrap_or_else(|error| panic!("failed to remove {}: {error}", lock.display()));
}
for path in [
"PP-OCRv6_small/det/inference.onnx",
"PP-OCRv6_small/det/inference.yml",
"PP-OCRv6_small/rec/inference.onnx",
"PP-OCRv6_small/rec/inference.yml",
] {
assert!(
model_root.join(path).is_file(),
"packaged PP-OCRv6 model is missing {path}"
);
}
}
fn required_path(name: &str) -> PathBuf {
std::env::var_os(name)
.map(PathBuf::from)
.unwrap_or_else(|| panic!("{name} must point to the real Use checkout artifact"))
}
fn use_version(binary: &Path) -> String {
let output = Command::new(binary)
.arg("--version")
.output()
.unwrap_or_else(|error| panic!("failed to run {}: {error}", binary.display()));
assert!(
output.status.success(),
"{} --version failed: {}",
binary.display(),
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).expect("Use version output must be UTF-8");
stdout
.split_whitespace()
.last()
.filter(|version| !version.is_empty())
.map(str::to_string)
.expect("Use version output must end with a version")
}
fn copy_executable(source: &Path, destination: &Path) {
std::fs::copy(source, destination).unwrap_or_else(|error| {
panic!(
"failed to copy executable {} to {}: {error}",
source.display(),
destination.display()
)
});
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(destination, std::fs::Permissions::from_mode(0o755))
.unwrap_or_else(|error| panic!("failed to chmod {}: {error}", destination.display()));
}
}
fn copy_tree(source: &Path, destination: &Path) {
std::fs::create_dir_all(destination)
.unwrap_or_else(|error| panic!("failed to create {}: {error}", destination.display()));
for entry in std::fs::read_dir(source)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", source.display()))
{
let entry = entry.expect("read release source entry");
let file_type = entry.file_type().expect("inspect release source entry");
let target = destination.join(entry.file_name());
assert!(
!file_type.is_symlink(),
"release source must not contain symlinks: {}",
entry.path().display()
);
if file_type.is_dir() {
copy_tree(&entry.path(), &target);
} else if file_type.is_file() {
std::fs::copy(entry.path(), &target).unwrap_or_else(|error| {
panic!(
"failed to copy release file {} to {}: {error}",
entry.path().display(),
target.display()
)
});
} else {
panic!(
"release source contains an unsupported entry: {}",
entry.path().display()
);
}
}
}