feat: add prerequisites, offline prerequisite setup and version tabs
- 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>
This commit is contained in:
1 parent
366c8fe173
commit
4964f2ced4
110 files changed
+13960
-295
No files matched your search
@@ -0,0 +1,125 @@
|
||||
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"))));
|
||||
}
|
||||
Reference in new issue
Block a user