Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions changelog.d/7920-windows-ui-archive-dedup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
**Windows UI archive dedup no longer drops path-qualified native members.**
Rebuilding the deduplicated Windows UI archive flattened member names, so
equal basenames overwrote one another and WebView2LoaderStatic's
`obj/.../*.obj` members (plus part of `winspool.drv`) were silently omitted —
leaving `CreateCoreWebView2EnvironmentWithOptions` and friends undefined at
link. Members are now normalized to unique flat names, `.drv` members are
treated as import-library members alongside `.dll`, and `uxtheme.lib` /
`winspool.lib` / `rpcrt4.lib` come from the canonical system link line.
(Fragment added at merge; see the PR body for the full analysis.)
13 changes: 12 additions & 1 deletion crates/perry/src/commands/compile/link/windows_link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ pub(super) fn add_system_libs(cmd: &mut Command) {
.arg("shlwapi.lib")
.arg("ole32.lib")
.arg("comctl32.lib")
// Perry strips bundled `.dll` import-library members from the UI
// staticlib during runtime deduplication. Keep every UI import explicit
// here so the trimmed archive remains self-contained at final link:
// uxtheme provides SetWindowTheme, winspool supplies the print-dialog
// imports, and rpcrt4 provides UuidCreate (windows-core GUID::new).
.arg("uxtheme.lib")
.arg("winspool.lib")
.arg("rpcrt4.lib")
.arg("advapi32.lib")
.arg("comdlg32.lib")
.arg("ws2_32.lib")
Expand Down Expand Up @@ -80,7 +88,7 @@ mod tests {
use super::*;

#[test]
fn image_widget_system_imports_survive_the_staticlib_boundary() {
fn windows_ui_system_imports_survive_the_staticlib_boundary() {
let mut command = Command::new("link.exe");
add_system_libs(&mut command);
let args: Vec<_> = command
Expand All @@ -89,6 +97,9 @@ mod tests {
.collect();
assert!(args.iter().any(|arg| arg == "shlwapi.lib"));
assert!(args.iter().any(|arg| arg == "winhttp.lib"));
assert!(args.iter().any(|arg| arg == "uxtheme.lib"));
assert!(args.iter().any(|arg| arg == "winspool.lib"));
assert!(args.iter().any(|arg| arg == "rpcrt4.lib"));
}
}

Expand Down
81 changes: 72 additions & 9 deletions crates/perry/src/commands/compile/strip_dedup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,38 @@ fn collect_archive_undefined_by_member(
)))
}

/// Locate a member after `llvm-ar x` extracted it into `extract_dir`.
///
/// COFF archives can preserve path-qualified member names (WebView2's loader
/// uses names such as `obj/.../loader_impl.obj`), but `llvm-ar x` writes those
/// members as their basename. Looking only at `extract_dir.join(member)` made
/// the extraction appear successful while silently omitting the object from
/// the rebuilt UI archive.
fn extracted_archive_member(extract_dir: &Path, member: &str) -> Option<PathBuf> {
let exact = extract_dir.join(member);
if exact.exists() {
return Some(exact);
}
Path::new(member)
.file_name()
.map(|name| extract_dir.join(name))
.filter(|path| path.exists())
}

/// Rust staticlibs can bundle Windows SDK import-library members named after
/// either a `.dll` or a `.drv` (notably the five same-named `winspool.drv`
/// members). These must come from Perry's canonical system-library link line:
/// extracting same-named import members one by one flattens/overwrites them and
/// leaves an incomplete descriptor/thunk set in the rebuilt archive.
fn is_windows_import_archive_member(member: &str) -> bool {
Path::new(member)
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| {
extension.eq_ignore_ascii_case("dll") || extension.eq_ignore_ascii_case("drv")
})
}

/// On Windows, build a trimmed UI lib using the rlib (not staticlib).
///
/// perry-ui-windows builds as both rlib and staticlib. The staticlib bundles
Expand Down Expand Up @@ -701,7 +733,7 @@ pub(super) fn strip_duplicate_objects_from_lib(lib_path: &PathBuf) -> Result<Pat
let ui_only_deps: Vec<&String> = staticlib_members
.iter()
.filter(|m| {
if m.ends_with(".dll") {
if is_windows_import_archive_member(m) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Restrict import-member filtering to Windows archives.

This check runs for every archive. If a non-Windows archive contains an ordinary member named foo.dll or foo.drv, the code drops it as an import member. Gate the predicate with is_win_lib.

Suggested fix
-            if is_windows_import_archive_member(m) {
+            if is_win_lib && is_windows_import_archive_member(m) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if is_windows_import_archive_member(m) {
if is_win_lib && is_windows_import_archive_member(m) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/strip_dedup.rs` at line 736, Update the
archive member filtering condition around is_windows_import_archive_member(m) to
also require is_win_lib, so import-member removal only occurs for Windows
archives while ordinary foo.dll or foo.drv members in other archives remain
intact.

return false;
}
if m.contains("compiler_builtins") {
Expand Down Expand Up @@ -769,7 +801,7 @@ pub(super) fn strip_duplicate_objects_from_lib(lib_path: &PathBuf) -> Result<Pat
let abs_rlib = std::fs::canonicalize(&rlib_path)?;
let mut rlib_extracted = 0usize;
let mut rlib_skipped = 0usize;
for member in &rlib_objects {
for (member_index, member) in rlib_objects.iter().enumerate() {
let is_alloc_shim = !member.contains(".cgu.") && !member.contains("-cgu.");
if is_alloc_shim {
rlib_skipped += 1;
Expand All @@ -782,9 +814,14 @@ pub(super) fn strip_duplicate_objects_from_lib(lib_path: &PathBuf) -> Result<Pat
.current_dir(&extract_dir)
.output()?;
if out.status.success() {
let p = extract_dir.join(member);
if p.exists() {
all_objects.push(p);
if let Some(extracted) = extracted_archive_member(&extract_dir, member) {
// Move every extracted object to a unique flat name before
// extracting the next member. Two path-qualified members
// may share a basename, and llvm-ar would otherwise
// overwrite the earlier one.
let normalized = extract_dir.join(format!("rlib_{member_index}.obj"));
std::fs::rename(extracted, &normalized)?;
all_objects.push(normalized);
Comment on lines +817 to +824

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Return an error when any member extraction fails.

When llvm-ar succeeds but extracted_archive_member returns None, the rlib loop silently omits the member. The staticlib loop records the failure but continues to rebuild_archive. The downstream code in crates/perry/src/commands/compile/link/build_and_run.rs at Lines 985-1010 uses the rebuilt archive for Ok and falls back to the original archive only for Err. This can produce an incomplete UI archive and remove required symbols.

Count failures in both loops. Return Err before rebuilding when any extraction fails.

Suggested failure handling
+    let mut extract_fail = 0usize;
+
     // Extract rlib members.
...
             if let Some(extracted) = extracted_archive_member(&extract_dir, member) {
                 ...
                 all_objects.push(normalized);
                 rlib_extracted += 1;
+            } else {
+                extract_fail += 1;
             }
+        } else {
+            extract_fail += 1;
         }
...
     if extract_fail > 0 {
         eprintln!("[strip-dedup] WARNING: {extract_fail} members failed to extract from staticlib");
+        return Err(anyhow::anyhow!(
+            "failed to extract {extract_fail} archive members"
+        ));
     }

Also applies to: 838-859

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/src/commands/compile/strip_dedup.rs` around lines 817 - 824,
Update the rlib extraction loop around extracted_archive_member and the
corresponding staticlib loop to count every member for which extraction returns
None, while preserving the existing successful-object handling. Before calling
rebuild_archive, return an error when either loop recorded any extraction
failures, so incomplete archives are never rebuilt or returned as successful.

rlib_extracted += 1;
}
}
Expand All @@ -798,17 +835,20 @@ pub(super) fn strip_duplicate_objects_from_lib(lib_path: &PathBuf) -> Result<Pat
// is read (the warning below); the parallel `extract_ok` counter
// was incremented but never reported. Dropped.
let mut extract_fail = 0usize;
for member in &ui_only_deps {
for (member_index, member) in ui_only_deps.iter().enumerate() {
let out = Command::new(&llvm_ar)
.arg("x")
.arg(&abs_staticlib)
.arg(member.as_str())
.current_dir(&extract_dir)
.output()?;
if out.status.success() {
let p = extract_dir.join(member.as_str());
if p.exists() {
all_objects.push(p);
if let Some(extracted) = extracted_archive_member(&extract_dir, member) {
let normalized = extract_dir.join(format!("static_{member_index}.obj"));
std::fs::rename(extracted, &normalized)?;
all_objects.push(normalized);
} else {
extract_fail += 1;
}
} else {
extract_fail += 1;
Expand Down Expand Up @@ -1849,6 +1889,29 @@ empty_marker.o:
assert!(symbols.contains("ui_only_symbol"));
assert!(!symbols.contains("runtime_canonical"));
}

#[test]
fn extracted_path_qualified_archive_member_falls_back_to_basename() {
let temp = tempfile::tempdir().unwrap();
let extracted = temp.path().join("loader_impl.obj");
std::fs::write(&extracted, b"native object fixture").unwrap();

assert_eq!(
super::extracted_archive_member(
temp.path(),
"obj/edge_embedded_browser/client/win/WebView2LoaderLib/loader_impl.obj",
),
Some(extracted)
);
}

#[test]
fn windows_import_members_include_dll_and_driver_archives() {
assert!(super::is_windows_import_archive_member("uxtheme.dll"));
assert!(super::is_windows_import_archive_member("winspool.drv"));
assert!(super::is_windows_import_archive_member("WINSPool.DRV"));
assert!(!super::is_windows_import_archive_member("loader_impl.obj"));
}
}

#[cfg(test)]
Expand Down
Loading