- EXE versions: prerequisite list with the built-in catalog, custom installers, download link checks and install file analysis; entries are added by the user (no automatic adding), one "추가" dialog with 기본 목록 / 직접 추가 tabs - offline prerequisite setup: single exe made from PrerequisiteSetup.exe (Windows task dialog, no prerequisites) plus every installer, hash checked before, while and after appending; optional WebView2 Runtime chosen in the save dialog; replaces an existing file only after the user agrees (CLI --replace) - desktop: progress popup with 생성위치 열기, save dialog fix for verbatim paths, EXE 서명 and 설치파일 생성 as version tabs, start screen 도구 › 서명하기 placeholder - CLAUDE.md: results are overwritten only with the user's consent - test kit and scripts for Windows Sandbox and Hyper-V checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
4.6 KiB
Rust
126 lines
4.6 KiB
Rust
use package_domain::{FileRule, ProjectKind};
|
|
use package_infra as infra;
|
|
use std::{fs, path::Path};
|
|
|
|
fn rule(id: &str, include: &[&str], required: bool) -> FileRule {
|
|
FileRule {
|
|
id: id.into(),
|
|
source: "appPayload".into(),
|
|
source_folder: String::new(),
|
|
target_root: String::new(),
|
|
include: include.iter().map(|p| (*p).into()).collect(),
|
|
exclude: Vec::new(),
|
|
target_relative_path: String::new(),
|
|
required,
|
|
}
|
|
}
|
|
|
|
fn write(root: &Path, relative: &str, bytes: &[u8]) {
|
|
let path = root.join(relative);
|
|
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
fs::write(path, bytes).unwrap();
|
|
}
|
|
|
|
/// A Windows program header whose optional header has no known format.
|
|
fn damaged_program() -> Vec<u8> {
|
|
let mut bytes = vec![0u8; 0x40];
|
|
bytes[..2].copy_from_slice(b"MZ");
|
|
bytes[0x3c..0x40].copy_from_slice(&0x40u32.to_le_bytes());
|
|
bytes.extend(b"PE\0\0");
|
|
let mut coff = [0u8; 20];
|
|
coff[16..18].copy_from_slice(&2u16.to_le_bytes());
|
|
bytes.extend(coff);
|
|
bytes.extend([0, 0]);
|
|
bytes
|
|
}
|
|
|
|
fn project(temp: &Path) -> (infra::ProjectDocument, std::path::PathBuf) {
|
|
let mut doc = infra::create_project(&temp.join("tool/Tool.isproj"), ProjectKind::Exe).unwrap();
|
|
let payload = temp.join("payload");
|
|
fs::create_dir_all(&payload).unwrap();
|
|
doc.local
|
|
.sources
|
|
.insert("appPayload".into(), payload.to_str().unwrap().into());
|
|
(doc, payload)
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_configs_unreadable_programs_and_broken_rules_are_reported_without_hiding_the_rest() {
|
|
let temp = tempfile::tempdir().unwrap();
|
|
let (doc, payload) = project(temp.path());
|
|
write(
|
|
&payload,
|
|
"App.runtimeconfig.json",
|
|
br#"{"runtimeOptions":{"tfm":"net8.0","frameworks":[{"name":"Microsoft.NETCore.App","version":"8.0.0"},{"name":"Microsoft.WindowsDesktop.App","version":"8.0.0"}]}}"#,
|
|
);
|
|
write(&payload, "Self.runtimeconfig.json", br#"{"runtimeOptions":{"includedFrameworks":[{"name":"Microsoft.NETCore.App","version":"8.0.0"}]}}"#);
|
|
write(&payload, "Broken.dll", &damaged_program());
|
|
write(&payload, "Bad.runtimeconfig.json", b"{");
|
|
write(&payload, "readme.txt", b"MZ");
|
|
let rules = [
|
|
rule("app", &["*"], true),
|
|
rule("gone", &["missing.*"], true),
|
|
];
|
|
let scan = infra::scan_prerequisites(&rules, &doc.local, doc.path.parent().unwrap(), "x86");
|
|
let found: Vec<_> = scan.found.iter().map(|item| item.id.as_str()).collect();
|
|
assert_eq!(
|
|
found,
|
|
["dotnet8-desktop-x86"],
|
|
"the project architecture stands in without App.exe"
|
|
);
|
|
assert_eq!(scan.found[0].reasons.len(), 2);
|
|
assert!(
|
|
scan.notes.is_empty() && scan.unsupported.is_empty(),
|
|
"{scan:?}"
|
|
);
|
|
assert_eq!(scan.problems.len(), 4, "{:?}", scan.problems);
|
|
assert!(scan.problems[0].starts_with("IncompleteFileList: 파일 규칙 1개를"));
|
|
assert!(
|
|
scan.problems[1].contains("gone") && scan.problems[1].contains("MissingRequiredFile"),
|
|
"the unread rule names itself: {}",
|
|
scan.problems[1]
|
|
);
|
|
assert!(scan
|
|
.problems
|
|
.iter()
|
|
.any(|p| p.starts_with("Bad.runtimeconfig.json: InvalidRuntimeConfig")));
|
|
assert!(scan
|
|
.problems
|
|
.iter()
|
|
.any(|p| p.starts_with("Broken.dll: InvalidPeFile")));
|
|
}
|
|
|
|
/// The test program itself is built by MSVC with the shared C runtime, so it imports VCRUNTIME140.dll.
|
|
#[cfg(all(windows, target_env = "msvc"))]
|
|
#[test]
|
|
fn a_real_program_built_with_visual_cpp_asks_for_the_runtime_unless_it_ships_it() {
|
|
let temp = tempfile::tempdir().unwrap();
|
|
let (doc, payload) = project(temp.path());
|
|
fs::copy(std::env::current_exe().unwrap(), payload.join("App.exe")).unwrap();
|
|
let arch = if cfg!(target_arch = "x86") {
|
|
"x86"
|
|
} else {
|
|
"x64"
|
|
};
|
|
let root = doc.path.parent().unwrap();
|
|
let scan = infra::scan_prerequisites(&[rule("app", &["*"], true)], &doc.local, root, "x64");
|
|
assert!(scan.problems.is_empty(), "{:?}", scan.problems);
|
|
let vc = scan
|
|
.found
|
|
.iter()
|
|
.find(|item| item.id == format!("vc14-{arch}"))
|
|
.expect("Visual C++ runtime");
|
|
assert!(vc.reasons.iter().all(|reason| reason.file == "App.exe"));
|
|
|
|
write(&payload, "vcruntime140.dll", b"app-local copy");
|
|
let shipped = infra::scan_prerequisites(&[rule("app", &["*"], true)], &doc.local, root, "x64");
|
|
let vc = shipped
|
|
.found
|
|
.iter()
|
|
.find(|item| item.id.starts_with("vc14-"));
|
|
assert!(vc.is_none_or(|item| item
|
|
.reasons
|
|
.iter()
|
|
.all(|r| !r.clue.to_lowercase().starts_with("vcruntime140.dll"))));
|
|
}
|