fix(pe): scope the hostLibs strip by DECLARATION, not by provenance

The payload census is gone.  It classified "regular file in $DRV_PATH = the
build produced it, symlink = a dependency travelling with it", and on the shape
this feature exists for that reading is simply wrong: in the real
logos-package_manager-module x86_64-w64-mingw32 output, 8 of the 19 DLLs in lib/
(icudt76, icuuc76, libstdc++-6, libgcc_s_seh-1, libmcfgthread-2, libsodium-26,
zlib1 ...) are REGULAR FILES.  The rule called the entire host runtime payload
and stripped nothing.  It also still emptied an extraDirs entry whose DLL
happened to be a symlink.

In its place, three rules that infer nothing:

  * a CALLER's pattern deletes — including one that matches the package's own
    build products.  `hostLibs = [ "Qt*" ]` means what it says;
  * a BUNDLER-injected pattern never reaches the PE path: flake.nix's qtPlugin
    appends `Qt*` only on a non-Windows target, where hostLibs can only filter
    what gets ADDED.  That is where the 816-PEs-in/796-out silent deletion came
    from, closed at the source;
  * `extraDirs` is never touched.  No under-strip follows: a host-matched import
    is skipped BEFORE the closure lookup, so the sweep never stages one into an
    extraDir in the first place.

An app-shaped bundle is now REFUSED the moment hostLibs would drop anything.
The premise under every claim is that the loading process's own directory is the
host's; when the bundle has an .exe that directory is the bundle's own bin/.
Measured on Windows 11: such a bundle dies 0xC0000135 before main() with no
output, and runs only with the host's bin\ on PATH.  Detection is by
IMAGE_FILE_HEADER.Characteristics, not by the .exe suffix.

Defects closed alongside:
  * the sixth false comment, written inside the fix for the fifth: a non-PE file
    named Qt6Core.dll does NOT reach the Qt-detection arm — the loop's own
    `[ -f ] && ! pe_is_pe` skips it.  Only a DIRECTORY falls through, and that
    is now the demonstrating subject;
  * README's "extraDirs is never touched" is true rather than deleted;
  * the Phase 1d comment goes with the census, no fragment left;
  * two refusal arms nothing reached now have subjects: hostBundle on an
    unknown target (stdenv removed) and hostBundle that is not a directory;
  * smoke.sh's must-fail table said TAB over an IFS='|' reader.

The LoadLibraryEx table in README/mkBundle/bundle.sh is REPLACED, not annotated.
Re-measured on Windows 11 against the real module with the unstripped bundle as
control: only two rows are attributable to stripping at all, 0x0000 fails on the
control too (a real module keeps private DLLs beside it), and 0x0008 loads when
the host's CWD is its own bin\.  The old table came from a synthetic one-DLL
module and reported 0x0000 as loading.

Evidence: 22/22 PE subjects (11 build + 11 refused, each by the message its own
arm prints); the three new guards each demonstrated FIRING by removing them one
at a time; Unix path byte-identical to origin/main across 9 ELF subjects (NAR +
`nix log` stdout), with a positive control flagging all 9 and a null-change
control flagging none; and on real Windows the real module 86M -> 16M, loading
under 0x1100 against its host, failing 126 against a host missing one claimed
DLL while the unstripped control loads from that same broken host.
This commit is contained in:
Dario Gabriel Lipicar
2026-08-12 18:49:18 -03:00
parent fcf4bddb9a
commit 99a41904d1
6 changed files with 628 additions and 382 deletions
+54 -24
View File
@@ -72,45 +72,75 @@ mkBundle {
}
```
Both bundle shapes are supported: an application (`bin/`) and a module whose
whole output is `lib/<name>.dll`.
**Only for a bundle that is loaded into something else.** On Windows a
host-provided DLL is found because the loader searches the *loading process's*
own directory. That is the host's directory when this bundle is a module
(`lib/<name>.dll`, no executable of its own) and it is *this* bundle's `bin/`
when the bundle has an `.exe` — where the stripped DLLs no longer are. So a
`hostLibs` claim on a bundle that ships an executable is **refused at build
time**: measured, such a bundle dies with `0xC0000135` before `main()` and
prints nothing, and runs only with the host's `bin/` prepended to `PATH`.
**What `hostLibs` may remove.** On ELF and Mach-O it filters what the bundler
*adds*: a traced dependency matching the list is not copied in. On Windows the
bundler also has to *delete*, because nixpkgs' `win-dll-link.sh` has already
staged the import closure into the derivation's own output before the bundler
sees it. Deleting is the more dangerous operation, so it is bounded by a
provenance rule: **the strip may only remove a file that came along with the
package** — one the bundler staged, or one `win-dll-link.sh` linked in from
another store path — **never a file the derivation's own build produced.** A
Qt plugin package bundled with `hostLibs = [ "Qt*" ]` therefore loses the Qt
runtime DLLs and keeps `qtquick2plugin.dll`, and a DLL the caller carried in
via `extraDirs` is never touched.
sees it. Deleting is the more dangerous operation, so it is bounded by *who
declared what*:
- **A pattern you write deletes.** `hostLibs = [ "Qt*" ]` on a Qt plugin package
removes that package's own `Qt6*.dll` too. That is the contract, not an
accident — and `hostBundle` is how you make it checkable.
- **A pattern the bundler injects does not.** `bundlers.<sys>.qtPlugin` appends
`Qt*` for you; it appends it only on non-Windows targets, where `hostLibs`
cannot delete anything.
- **`extraDirs` is never touched.** Those directories are named by you, one by
one, as "carry this"; a name glob does not overrule that.
- **A strip that would empty the bundle fails the build.**
Do not read anything into the shape of the patterns. Cross-platform spellings
*can* over-match — `libcrypto*` matches `libcrypto-3-x64.dll` — and the safety
comes from the payload rule, from `hostBundle`, and from the build failing when
a strip would empty the bundle.
comes from the rules above, from `hostBundle`, and from the log naming every
dropped file.
**The host must also LOOK there.** A stripped module finds the host's DLLs
because Windows searches the *loading process's own directory*, and some
`LoadLibraryEx` flags remove precisely that directory from the search. Measured
on Windows 11 x86-64 with one stripped module and one unstripped control:
`LoadLibraryEx` flags remove precisely that directory from the search.
| flags | what it means | stripped module | control |
|---|---|---|---|
| `0x0000` | default search order | **loads** | loads |
| `0x0008` | `LOAD_WITH_ALTERED_SEARCH_PATH` | fails, 126 | loads |
| `0x0100` | `LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR` alone | fails, 126 | loads |
| `0x1100` | `…DLL_LOAD_DIR|…DEFAULT_DIRS` | **loads** | loads |
Measured on Windows 11 x86-64 against the **real** `logos-package_manager`
module (16 DLLs stripped, 3 kept), a host bundle shipping those 16, and the
*unstripped* bundle of the same module as the control. The loading process's
current directory holds none of the DLLs unless a row says otherwise:
| flags | what it means | stripped | control | what the pair says |
|---|---|---|---|---|
| `0x1100` | `…DLL_LOAD_DIR|…DEFAULT_DIRS` | **loads** | loads | the mode Logos uses; the strip is invisible |
| `0x0008` | `LOAD_WITH_ALTERED_SEARCH_PATH` | fails, 126 | **loads** | the strip, and only the strip |
| `0x0008` | same, CWD = the host's own `bin\` | **loads** | — | the current directory is still in that order |
| `0x0000` | default search order | fails, 126 | fails, 126 | *not* the strip |
| `0x0100` | `…SEARCH_DLL_LOAD_DIR` alone | fails, 126 | fails, 126 | *not* the strip |
| `0x1000` | `…SEARCH_DEFAULT_DIRS` alone | fails, 126 | fails, 126 | *not* the strip |
| `0x1100` | host missing one claimed DLL | fails, 126 | **loads** | the claim is load-bearing |
`0x1100` is what logos-module's `preloadPluginWithOwnDirSearch`
(`src/win_dll_search.h`) passes, which is why Logos modules may be stripped.
The two failing modes substitute the module's own directory *for* the
application directory instead of adding to it. Nothing at build time can see
which mode a host will use, so this is a property of the host you must check
once, by hand — `hostBundle` checks that the DLL is *there*, not that anyone
will look.
Read the table as a statement about which directories a mode searches, not as
one verdict per flag — the three "not the strip" rows are why. A real module
keeps private DLLs of its own beside it, and the default order does not search a
loaded DLL's own directory at all, so `0x0000` fails on the *unstripped* bundle
too; `0x0100` and `0x1000` each name only half of what is needed. And `0x0008`
replaces the application directory while leaving the rest of the standard order
in place, current directory included — so the same host, started from its own
`bin\`, loads the same stripped module through `0x0008`.
(An earlier revision of this table was measured on a synthetic single-DLL module
and reported `0x0000` as loading. That is true only for a module with no private
dependencies of its own, which no real Logos module is.)
Nothing at build time can see which mode a host will use, or where it will be
started from, so this is a property of the host you must check once, by hand —
`hostBundle` checks that the DLL is *there*, not that anyone will look.
Every `hostLibs` entry is a promise about another package's output, and on
Windows a broken promise is silent — `LoadLibrary` fails with
+284 -196
View File
@@ -142,93 +142,21 @@ for dir in "${extra_dirs[@]+"${extra_dirs[@]}"}"; do
fi
done
# ---------------------------------------------------------------------------
# Phase 1d — which files here did the DERIVATION ITSELF produce?
# ---------------------------------------------------------------------------
# The census that separates this package's PAYLOAD from the dependencies that
# merely travelled with it, and it has to be taken HERE — the last moment when
# `$out` holds Phase 1's copy of `$DRV_PATH` and nothing else. Everything the
# bundler adds after this point (the PE sweep's closure copies, Phase 2b's Qt
# plugin and QML trees, the mirror into bin/) is by construction not payload,
# which is why the answer is "was it in the census" and not a second walk.
# The set of directories the caller asked for BY NAME, as absolute paths in
# `$out`. Read by `pe_in_extra_dir` (the PE hostLibs strip); nothing else needs
# it, because nothing else deletes.
#
# WHY the distinction exists at all: `hostLibs` on the Unix path only ever
# FILTERS WHAT GETS ADDED — `is_host_lib` is consulted inside `trace_deps`,
# which copies dependencies in, and nothing on that path ever deletes something
# Phase 1 wrote. The PE path needs to DELETE, because on Windows the
# dependency closure has already been staged into the derivation's own output
# before the bundler ever sees it: nixpkgs' win-dll-link.sh runs in fixup and
# links every DLL an import table names into `$out/bin` (and, where a package
# calls `linkDLLsInfolder` itself, `$out/lib`). Deleting and filtering are not
# the same operation, and treating them as one is how a pattern that is inert
# on Unix becomes destructive on Windows.
#
# So the rule the strip enforces is: it may remove a DEPENDENCY that came along
# with this package, never a file the package's own build produced. The signal
# is exact and needs no heuristics, because win-dll-link.sh stages by
# `ln -sr` — a SYMLINK into another store path — while a derivation's own build
# products are real files in its own output. Phase 1 copies with `cp -aL`, so
# by the time a file is in `$out` both look alike; asking `$DRV_PATH` before
# that distinction is lost is the whole point of taking the census here.
#
# Honest limit: a derivation whose output is entirely symlinks into OTHER store
# paths (`symlinkJoin`, and buildEnv-shaped outputs generally) has, by this
# reading, no payload of its own. That is literally true — it produced no file
# — and the consequence is that `hostLibs` governs its whole tree, exactly as
# it did before this census existed. The floor in `pe_strip_host_libs` is what
# stands under that case.
declare -A pe_payload # absolute path in $out -> 1, the drv's own output
pe_payload_own=0 # files the derivation produced
pe_payload_linked=0 # files it only linked in from another store path
pe_payload_censused=""
pe_payload_census() {
[ -n "$pe_payload_censused" ] && return 0
pe_payload_censused=1
local drv_real f rel src src_real files
# Canonicalised on both sides: `$DRV_PATH` may itself be reached through a
# symlink, and a prefix test between a canonical path and a non-canonical one
# answers "not payload" for the derivation's entire output.
drv_real="$(readlink -f "$DRV_PATH")" || {
echo " ERROR: could not canonicalise DRV_PATH ($DRV_PATH); refusing to" >&2
echo " classify anything as this derivation's own output on a failed read." >&2
exit 1
}
files="$(find "$out" -type f)" || {
echo " ERROR: could not walk $out to take the payload census; refusing to" >&2
echo " decide what may be stripped on the strength of a failed scan." >&2
exit 1
}
while IFS= read -r f; do
[ -n "$f" ] || continue
rel="${f#"$out"/}"
src="$DRV_PATH/$rel"
# No counterpart in the derivation at all: not something Phase 1 copied, so
# not payload. Nothing produces this today (Phase 1 is the only writer so
# far) and it is handled rather than assumed.
[ -e "$src" ] || continue
src_real="$(readlink -f "$src")" || continue
case "$src_real" in
"$drv_real"|"$drv_real"/*)
pe_payload["$f"]=1
pe_payload_own=$((pe_payload_own + 1))
;;
*)
pe_payload_linked=$((pe_payload_linked + 1))
;;
esac
done <<< "$files"
return 0
}
# Taken unconditionally except where Nix has already said the target is Unix,
# so that stdout on the ELF/Mach-O path is byte-for-byte what it was: this
# function prints nothing, and the summary line is emitted by the PE section
# that consults it. "unknown" still takes the census, because Phase 1c may yet
# resolve it to Windows.
if [ "${IS_WINDOWS:-unknown}" != "0" ]; then
pe_payload_census
fi
# Normalised here rather than at each use: `extraDirs = [ "share/assets/" ]` and
# `[ "share/assets" ]` name the same directory, and a previous prune elsewhere in
# this file compared the DECLARED string literally, so the trailing slash decided
# whether it matched at all. Leading `./` is stripped for the same reason.
extra_dir_roots=()
for dir in "${extra_dirs[@]+"${extra_dirs[@]}"}"; do
dir="${dir#./}"
while [ "${dir%/}" != "$dir" ]; do dir="${dir%/}"; done
[ -n "$dir" ] || continue
extra_dir_roots+=("$out/$dir")
done
# ===========================================================================
# Phase 1c — What TARGET is this bundle for?
@@ -374,12 +302,6 @@ elif [ "$IS_WINDOWS" = "1" ]; then
exit 1
fi
echo " Output census: $tree_pe_count PE file(s), $tree_unix_count ELF/Mach-O file(s)"
# The Phase 1d census, reported where it can be read against the one above.
# `$pe_payload_linked` is the interesting number: those are the files
# win-dll-link.sh linked into the derivation's output from somewhere else,
# i.e. exactly the set `hostLibs` is allowed to remove from.
echo " Payload census: $pe_payload_own file(s) this derivation produced," \
"$pe_payload_linked linked in from other store paths"
fi
# `hostBundle` is a PE-path argument, and a bundle that was handed one on a
@@ -628,6 +550,44 @@ pe_arch_or_die() {
exit 1
}
# --- is this PE an EXECUTABLE IMAGE, or a DLL? ------------------------------
# IMAGE_FILE_HEADER.Characteristics, bit IMAGE_FILE_DLL (0x2000), is the 2 LE
# bytes at lfanew+22. Read from the FILE FORMAT for the same reason pe_arch is:
# `file` says "(DLL)" today and its wording is not pinned, and the NAME is not
# consulted either — a Windows executable is one because of this bit, not
# because it ends in .exe.
#
# Used by one caller: the app-shape refusal below. An image whose header cannot
# be read answers YES, which is the refusing direction — the alternative is to
# let a bundle that may be its own process through a check that exists precisely
# because such a bundle cannot find the host's DLLs.
declare -A pe_exe_cache
pe_is_exe_image() {
local f="$1" bytes lfanew ch ans
pe_is_pe "$f" || return 1
ans="${pe_exe_cache["$f"]:-}"
if [ -z "$ans" ]; then
ans="yes"
# shellcheck disable=SC2207
bytes=($(od -An -tu1 -j60 -N4 -- "$f" 2>/dev/null)) || bytes=()
if [ "${#bytes[@]}" -eq 4 ]; then
lfanew=$(( bytes[0] + (bytes[1] << 8) + (bytes[2] << 16) + (bytes[3] << 24) ))
if [ "$lfanew" -gt 0 ]; then
# 24 bytes: "PE\0\0" + the whole 20-byte IMAGE_FILE_HEADER.
# shellcheck disable=SC2207
bytes=($(od -An -tu1 -j"$lfanew" -N24 -- "$f" 2>/dev/null)) || bytes=()
if [ "${#bytes[@]}" -eq 24 ] && [ "${bytes[0]}" -eq 80 ] && [ "${bytes[1]}" -eq 69 ] \
&& [ "${bytes[2]}" -eq 0 ] && [ "${bytes[3]}" -eq 0 ]; then
ch=$(( bytes[22] + (bytes[23] << 8) ))
[ $(( ch & 8192 )) -ne 0 ] && ans="no"
fi
fi
fi
pe_exe_cache["$f"]="$ans"
fi
[ "$ans" = "yes" ]
}
# --- does this bundle render QML? -------------------------------------------
# Defined HERE, with the other PE helpers, rather than inside Phase 2b's
# `qt_detected` branch where it used to live: Phase 6 needs the same predicate,
@@ -1023,10 +983,11 @@ pe_dir_index() {
# under-match (`libz*` really cannot match `zlib1.dll`, and `libstdc++.so*`
# really cannot match `libstdc++-6.dll`), but "some do" is not "none can", and
# nothing about this design may rest on the stronger claim. What actually
# keeps an over-broad pattern from being fatal is the payload rule in
# `pe_strip_host_libs` — a pattern can only ever remove a dependency that came
# along with the package, never something the package's own build produced —
# plus `hostBundle`, plus the floor. Not the shape of the glob.
# keeps an over-broad pattern honest is `hostBundle` — every name dropped has to
# be in the host's application directory — plus the ledger printed at the end of
# Phase 6 when there is no hostBundle to ask, plus the floor in
# `pe_strip_host_libs`. Not the shape of the glob. A caller who declares
# `*.dll` gets `*.dll`; what they do not get is silence about it.
#
# What is PE-specific is the MATCH, not the list. `is_host_lib` (Unix) is
# case-sensitive and stays byte-for-byte what it was. PE import tables spell
@@ -1086,7 +1047,7 @@ declare -A pe_stripped # absolute path this build REMOVED as host-provided
pe_host_indexed=""
pe_host_app_dir=""
pe_host_dropped_total=0
pe_host_payload_total=0 # matched hostLibs, kept because the drv produced it
pe_host_declared_total=0 # matched hostLibs, kept because extraDirs named it
pe_host_index() {
[ -n "$pe_host_indexed" ] && return 0
@@ -1147,9 +1108,76 @@ pe_host_index() {
return 0
}
# --- can this bundle be its OWN process? ------------------------------------
# The premise under every hostLibs claim is that the DLL will be found in the
# HOST PROCESS's own directory. That premise holds for a module — something
# else runs, and this bundle is loaded into it — and it does not hold for an
# application, because the process is then the bundle's own .exe and the
# directory Windows searches is the bundle's own bin/, where the stripped DLLs
# no longer are.
#
# Measured on Windows 11 x86-64: an app bundle stripped this way, run from its
# own bin/, dies with 0xC0000135 (STATUS_DLL_NOT_FOUND) and prints NOTHING, and
# runs only when the host's bin/ is prepended to PATH — i.e. when the deployment
# does something no build-time check can see. `hostBundle` cannot catch it
# either: it verifies a directory that, for this shape, the loader never
# consults.
#
# So it is refused, and refused rather than documented because the failure is
# silent in both of the ways this file cares about — no output at load time, and
# nothing wrong-looking in the bundle. The refusal is deliberately coarse: any
# executable image in the application directory means the bundle can be the
# process, whether or not THAT exe is the one that would need the stripped DLL,
# because "which exe will be launched, and what will it load" is not a question
# a bundler can answer. A bundle with a DLL-only bin/ (the shape win-dll-link.sh
# produces for a library package) is not refused: nothing in it can be a process.
pe_own_exe=""
pe_own_exe_resolved=""
pe_resolve_own_exe() {
[ -n "$pe_own_exe_resolved" ] && return 0
pe_own_exe_resolved=1
pe_resolve_app_dir
[ -n "$pe_app_dir" ] || return 0
local f files
files="$(find "$pe_app_dir" -maxdepth 1 -type f | sort)" || {
echo " ERROR: could not list $pe_app_dir; refusing to decide whether this" >&2
echo " bundle can be its own process on the strength of a failed scan." >&2
exit 1
}
while IFS= read -r f; do
[ -n "$f" ] || continue
pe_is_exe_image "$f" || continue
pe_own_exe="$f"
break
done <<< "$files"
return 0
}
pe_refuse_host_claim_on_app() {
local name="$1" why="$2"
pe_resolve_own_exe
[ -n "$pe_own_exe" ] || return 0
echo " ERROR: hostLibs would assign $name to the host ($why)," >&2
echo " but this bundle contains an executable of its own:" >&2
echo " ${pe_own_exe#"$out"/}" >&2
echo " A host-provided DLL is found because Windows searches the LOADING" >&2
echo " PROCESS's own directory. When the process is this bundle's own .exe" >&2
echo " that directory is this bundle's ${pe_app_dir#"$out"/}/, not the host's," >&2
echo " so every name dropped here is simply missing: 0xC0000135 before" >&2
echo " main(), with no output at all. Measured on Windows 11; the same" >&2
echo " bundle runs only with the host's bin/ prepended to PATH." >&2
echo " hostBundle cannot rescue it either — it would verify a directory the" >&2
echo " loader never consults for this shape." >&2
echo " hostLibs is for a bundle that is LOADED INTO a host (a module, a" >&2
echo " plugin: lib/<name>.dll and no executable). For an application, drop" >&2
echo " hostLibs and let the bundle carry what it imports." >&2
exit 1
}
# Record — and, when a hostBundle was given, PROVE — that the host ships $1.
pe_assert_host_provides() {
local name="$1" why="$2" key
pe_refuse_host_claim_on_app "$name" "$why"
key="${name,,}"
[ -n "${pe_host_claims["$name"]:-}" ] || pe_host_claims["$name"]="$why"
[ -n "${HOST_BUNDLE:-}" ] || return 0
@@ -1172,7 +1200,23 @@ pe_assert_host_provides() {
exit 1
}
# Remove from the staged tree every DEPENDENCY the host already provides.
# Is this path inside a directory the caller named in `extraDirs`?
#
# Prefix test on the STAGING ROOT, not on the declared string: `$out/share/assets`
# vs `share/assets/`. The roots were normalised once, next to Phase 1b's copy
# loop.
pe_in_extra_dir() {
local f="$1" root
for root in "${extra_dir_roots[@]+"${extra_dir_roots[@]}"}"; do
case "$f" in
"$root"/*) return 0 ;;
esac
done
return 1
}
# Remove from the staged tree every file the CALLER's `hostLibs` says the host
# already provides.
#
# This is the half that shrinks the package. Skipping a host-provided name
# during the sweep stops the bundler PULLING one in; it does nothing about the
@@ -1181,33 +1225,47 @@ pe_assert_host_provides() {
# derivation's own lib/, so a Logos module arrives here carrying ~36 MB of Qt,
# OpenSSL and C++ runtime (Qt6Core.dll alone is 15 MB) that the host ships.
#
# THE PAYLOAD RULE, which is what keeps "delete" from being a different and
# more dangerous operation than the Unix path's "do not add":
# WHAT BOUNDS IT, since deleting is not the same operation as the Unix path's
# "do not add". The rule is about who DECLARED the pattern and what the caller
# asked to carry — it infers nothing about a file from how it got here:
#
# this may only remove a file that came along WITH the package — one the
# bundler staged, or one win-dll-link.sh linked into the derivation's output
# from another store path — and never a file the derivation's own build
# produced.
# 1. Only a CALLER's pattern may delete. A pattern a bundler injects on the
# caller's behalf never reaches this function: `bundlers.<sys>.qtPlugin`
# appends `Qt*` to hostLibs itself, which is meaningful only on Unix (where
# hostLibs filters what gets ADDED), and flake.nix now appends it only on a
# non-Windows target. Measured on the real Windows Qt bundle while that
# injection still reached here: 816 PEs in, 796 out, exit 0 — the bundler
# deleting the very plugins it was asked to package, silently, because the
# PE match folds case and `Qt*` reads as `qt*` against `qtquick2plugin.dll`.
# 2. `extraDirs` is never touched. Those directories are named by the caller,
# one by one, as "carry this"; their contents are the caller's, whatever a
# hostLibs glob makes of the file names. Measured before this exception: a
# module with `extraDirs = [ "share/assets" ]` holding a DLL shipped
# `share/assets/` EMPTY, rc=0.
#
# `pe_payload` (Phase 1d) is that set, taken before anything else wrote to
# `$out`. Two measured defects close on this one rule and neither closes on a
# rule about pattern syntax:
# The obvious objection — the sweep DOES stage a dependency into an
# extraDir when the importer lives there (see the Qt module-arm note), so
# is this exemption not now under-stripping? — does not arise, and the
# reason is the ORDER inside pe_sweep: a host-matched import is skipped at
# the import level BEFORE the closure lookup, so nothing hostLibs matches
# is ever staged, into an extraDir or anywhere else. Every host-matched
# file inside an extraDir got there in Phase 1, out of the derivation's own
# output, which is exactly the content the caller asked to carry. If that
# ever stops holding, handle it explicitly here; do not go back to
# inferring it from how the file looks.
# 3. Everything else a caller-declared pattern matches goes. `hostLibs =
# [ "Qt*" ]` on a Qt plugin package removes that package's own Qt6*.dll,
# and that is the contract, not an accident: the caller declared it.
#
# * `bundlers.<sys>.qtPlugin` appends `Qt*` to hostLibs itself — a list the
# BUNDLER writes, not the caller — and the PE match folds case on both
# sides, so `Qt*` is `qt*` and matches `qtquick2plugin.dll`,
# `qtquickcontrols2plugin.dll`, `qtqmlmodelsplugin.dll`. Those files ARE
# the plugin bundle's payload. Measured on the real Windows Qt bundle
# before this rule: 816 PEs in, 796 out, exit 0 — the bundler deleting the
# thing it was asked to package, silently.
# * `extraDirs` names directories the caller explicitly asked to carry, and
# the strip walked `find "$out"` with no exception for them. Measured: a
# module with `extraDirs = [ "share/assets" ]` holding a DLL shipped
# `share/assets/` empty, rc=0. Not fixed by exempting extraDirs wholesale
# either — the sweep legitimately stages a dependency INTO an extraDir when
# the importer lives there (see the note above the Qt census), and those
# are still the host's to provide. Provenance separates the two; a path
# prefix cannot.
# An earlier revision bounded this by PROVENANCE instead — regular file in
# `$DRV_PATH` means the build produced it, symlink means a dependency travelling
# with it — and it is recorded here because it looks right and is not. On the
# shape this feature exists for it strips nothing: in
# `logos-package_manager-module`'s real x86_64-w64-mingw32 lib/, 8 of 19 DLLs
# (icudt76, icuuc76, libstdc++-6, libgcc_s_seh-1, libmcfgthread-2, libsodium-26
# and more) are REGULAR FILES, so the heuristic called the entire host runtime
# payload and the feature evaporated exactly where it applies. It also still
# deleted an extraDirs entry whose DLL happened to be a symlink.
#
# Called at the top of every sweep rather than once, because Phase 2b stages
# whole Qt plugin and QML trees between sweeps and those trees are copied
@@ -1216,7 +1274,7 @@ pe_assert_host_provides() {
# level, which is two answers to one question.
pe_strip_host_libs() {
local label="$1"
local f b files removed=0 kept=0 examined=0 payload_kept=0
local f b files removed=0 kept=0 examined=0 declared_kept=0
if [ -z "${host_patterns[*]+x}" ]; then
# hostBundle is validated even here, and BEFORE the "nothing to check"
# refusal below, so that a hostBundle which could never adjudicate anything
@@ -1258,15 +1316,16 @@ pe_strip_host_libs() {
kept=$((kept + 1))
continue
fi
# The payload rule. Named in the log rather than skipped quietly: a
# hostLibs entry that matches the package's own output is a caller mistake
# even when it is now harmless, and it is the one thing about this list
# that a reader cannot work out from the bundle afterwards.
if [ -n "${pe_payload["$f"]:-}" ]; then
# Rule 2: a directory the caller named in extraDirs is the caller's, and a
# name glob does not overrule "carry this". Named in the log rather than
# skipped quietly, because a hostLibs entry that matches inside an extraDir
# is the one thing about this list a reader cannot work out from the
# finished bundle.
if pe_in_extra_dir "$f"; then
kept=$((kept + 1))
payload_kept=$((payload_kept + 1))
echo " = ${f#"$out"/} (matches hostLibs, but this derivation's own" \
"build produced it — kept, and NOT claimed from the host)"
declared_kept=$((declared_kept + 1))
echo " = ${f#"$out"/} (matches hostLibs, but the caller named this" \
"directory in extraDirs — kept, and NOT claimed from the host)"
continue
fi
pe_assert_host_provides "$b" "staged in this bundle at ${f#"$out"/}"
@@ -1279,32 +1338,34 @@ pe_strip_host_libs() {
unset 'pe_arch_cache[$f]'
# Recorded by PATH, because Phase 6 has to tell "this build deliberately
# removed bin/x.dll" from "Phase 1 lost bin/x.dll", and re-testing the NAME
# there answers the wrong question: under the payload rule a bin/ entry can
# match hostLibs and still be required to be present.
# there answers the wrong question: an extraDirs entry can match hostLibs
# and still be required to be present.
pe_stripped["$f"]=1
removed=$((removed + 1))
echo " - ${f#"$out"/} (host-provided)"
done <<< "$files"
pe_host_dropped_total=$((pe_host_dropped_total + removed))
pe_host_payload_total=$payload_kept
pe_host_declared_total=$declared_kept
echo " Host-provided strip ($label): $examined PE(s) examined, $removed" \
"removed as host-provided, $kept kept" \
"($payload_kept of them this derivation's own output)"
# The floor, and it is now a BACKSTOP rather than the primary guard. What it
# was written for — an over-broad pattern such as `*.dll` eating the
# package's own payload — is what the payload rule above now refuses one file
# at a time, so on any derivation that produced a PE of its own this cannot
# fire. It is kept because one shape has no payload by that reading: a
# derivation whose whole output is symlinks into other store paths. There
# `hostLibs` still governs everything, and an over-broad list still empties
# the bundle. Demonstrated firing on exactly that shape
# (tests/pe-hostlibs.nix, `stripEverythingLinked`), not asserted.
"($declared_kept of them inside a declared extraDirs entry)"
# The floor. An over-broad list — `*.dll`, or a Unix spelling that happens to
# match everything — takes the package's own payload with it, and the caller
# is allowed to write exactly that: rule 3 says a declared pattern deletes.
# What is NOT allowed is doing it silently, and "the bundle now contains no PE
# at all" is the one case that needs no judgement about which file was the
# payload. Demonstrated firing rather than asserted (tests/pe-hostlibs.nix,
# `stripEverything`).
#
# It is a floor and not a ceiling: a list that removes all but one PE passes
# here. That is what `hostBundle` is for — every name that goes has to be in
# the host's application directory — and what the ledger at the end of Phase 6
# is for when there is no hostBundle to ask.
if [ "$examined" -gt 0 ] && [ "$kept" -eq 0 ]; then
echo " ERROR: the hostLibs strip removed every PE in this bundle" \
"($examined examined, $removed removed)." >&2
echo " Nothing this derivation's own build produced is in the bundle, so" >&2
echo " the payload rule had nothing to protect and the patterns took the" >&2
echo " whole tree. Nothing downstream of here would be measuring anything." >&2
echo " A bundle with nothing left in it cannot be what the caller meant," >&2
echo " and nothing downstream of here would be measuring anything." >&2
exit 1
fi
# Directory maps are memoised behind pe_dir_scanned and files just left the
@@ -1495,10 +1556,10 @@ pe_sweep() {
# It sits AFTER the two "already in the bundle" tests and BEFORE the
# closure lookup, and both halves of that placement are deliberate.
# After, because an import the bundle already satisfies is satisfied —
# under the payload rule a host-matched name can legitimately still be
# here (the derivation's own build produced it), and claiming it from
# the host as well would make the build fail over a DLL that is sitting
# right there. Before, because a host-provided name is not "missing":
# a host-matched name can legitimately still be here, in an extraDirs
# entry the strip may not touch, and claiming it from the host as well
# would make the build fail over a DLL that is sitting right there.
# Before, because a host-provided name is not "missing":
# left to the lookup it would be re-staged out of the closure, which is
# exactly what the strip had just undone.
if pe_is_host_lib "$imp"; then
@@ -2116,18 +2177,25 @@ if [ "$IS_WINDOWS" = "1" ] && [ -n "$pe_app_dir" ]; then
# name the same list had just matched — one list, two answers, in the same
# phase.
#
# This arm is REACHED and it changes the verdict. An earlier revision of
# this comment claimed the opposite — that the strip at the top of pass 1
# has already removed any Qt DLL the list claims, so a host-claimed name
# cannot get here — and that was false in two ways even then, and is more
# obviously false now:
# This arm is REACHED, on ONE shape, and it changes the verdict there. Two
# earlier revisions of this comment got its reachability wrong in opposite
# directions, so here is what is actually true. The strip at the top of
# pass 1 has already deleted every Qt-named PE the list matches, and it runs
# before this loop, so what can still be here and still match is:
#
# * the strip only considers PEs (`pe_is_pe`), so a non-PE file named
# `Qt6Core.dll` survives it and reaches this loop, which counts it for
# DETECTION on purpose (see the `[ -e ]` note above); and
# * under the payload rule the strip keeps a Qt-named DLL this
# derivation's own build produced, which is precisely the shape a Qt
# plugin package has.
# * a DIRECTORY named like a Qt DLL. The strip walks `find -type f` and
# cannot see one; this loop's `[ -e ]` counts it for DETECTION on
# purpose (see the note above). That is the shape
# tests/pe-hostlibs.nix demonstrates the arm with.
# * a Qt-named PE inside a directory the caller listed in `extraDirs`,
# which the strip may not touch — reachable only when the application
# directory itself is a declared extraDir.
#
# What is NOT reachable, and what the revision this replaces claimed was:
# "the strip only considers PEs, so a non-PE file named Qt6Core.dll
# survives it and reaches this loop". It does survive the strip — and then
# this loop's own `[ -f "$f" ] && ! pe_is_pe "$f"` skips it two lines above,
# before `qt_detected` is ever set. A non-PE regular file cannot get here.
#
# What happens when it does fire is the point: `qt_is_host=1` makes Phase
# 2b skip staging the Qt plugin and QML trees entirely. On a bundle whose
@@ -3640,11 +3708,15 @@ if [ "$IS_WINDOWS" = "1" ]; then
# twice.
#
# `pe_stripped`, keyed by PATH, and not `pe_is_host_lib "$n"`. Re-asking
# the name answers a question this arm is not allowed to ask any more:
# under the payload rule a bin/ entry can match hostLibs and still have
# been KEPT, and a name test would then wave through its disappearance as
# the name answers a question this arm is not allowed to ask: a bin/ entry
# can match hostLibs and still have been KEPT (an extraDirs entry, a
# non-PE), and a name test would then wave through its disappearance as
# deliberate when nothing deliberate happened to it. The only losses
# this arm may excuse are the ones this build performed.
#
# Reachable on a bin/ that holds no executable — the shape win-dll-link.sh
# produces for a library package, DLLs only. A bin/ with an .exe in it is
# refused outright by pe_refuse_host_claim_on_app long before here.
if [ -n "${pe_stripped["$out/bin/$n"]:-}" ]; then
win_host_bin=$((win_host_bin + 1))
continue
@@ -3739,10 +3811,10 @@ if [ "$IS_WINDOWS" = "1" ]; then
#
# Inside the `-z "$hit"` branch, not ahead of it: an import the bundle
# actually satisfies is resolved and gets the architecture check below,
# whatever hostLibs says about the name. Under the payload rule that
# case is real — a Qt-named DLL the derivation itself produced stays in
# the bundle — and adjudicating it as a host claim would fail the build
# over a file that is present.
# whatever hostLibs says about the name. That case is real — a
# host-matched DLL inside a declared extraDirs entry stays in the bundle
# — and adjudicating it as a host claim would fail the build over a file
# that is present.
#
# `pe_assert_host_provides`, not a bare `continue`, because a name that
# reaches here is one nothing in the bundle satisfies: the build ships
@@ -3858,27 +3930,43 @@ if [ "$IS_WINDOWS" = "1" ]; then
"each of these checked against the directory the host runs from."
fi
# The other half of the promise, and the half no build-time check can
# reach. Measured on real Windows 11 (x86-64), one stripped module and one
# unstripped control, four LoadLibraryEx search modes:
# reach. Measured on real Windows 11 (x86-64) against the REAL
# logos-package_manager module — 16 DLLs stripped, 3 kept — a host bundle
# shipping those 16, and the UNSTRIPPED bundle of the same module as the
# control, with the process's CURRENT DIRECTORY holding none of them:
#
# flags 0x0000 (default) stripped LOADS
# flags 0x0008 LOAD_WITH_ALTERED_SEARCH_PATH stripped FAILS 126
# flags 0x0100 SEARCH_DLL_LOAD_DIR alone stripped FAILS 126
# flags 0x1100 DLL_LOAD_DIR|DEFAULT_DIRS stripped LOADS
# stripped control
# flags 0x1100 DLL_LOAD_DIR|DEFAULT_DIRS LOADS LOADS
# flags 0x0008 ALTERED_SEARCH_PATH 126 LOADS <- the strip
# flags 0x0008, CWD = the host's bin\ LOADS -
# flags 0x0000 (default order) 126 126 <- not it
# flags 0x0100 SEARCH_DLL_LOAD_DIR alone 126 126 <- not it
# flags 0x1000 SEARCH_DEFAULT_DIRS alone 126 126 <- not it
# flags 0x1100, host missing one claim 126 LOADS <- the claim
#
# In every failing row the unstripped control loaded from the same
# directory, so the failure is the strip and not the machine. A
# host-provided DLL is found because the host process's OWN directory is
# searched, and two of the four modes remove exactly that directory from
# the search. logos-module passes 0x1100 on purpose
# (src/win_dll_search.h); a host that does not is a host these bundles
# cannot be installed under, and nothing at build time can tell.
# Only the rows where the CONTROL differs say anything about stripping.
# logos-module passes 0x1100 on purpose (src/win_dll_search.h), and that is
# the row that has to hold. A previous version of this table came from a
# synthetic one-DLL module and reported 0x0000 as loading; a real module
# keeps private DLLs beside itself, and the default order does not search a
# loaded DLL's own directory, so the unstripped control fails there too.
#
# The CWD qualifier is not a footnote either. 0x0008 SUBSTITUTES the
# module's directory for the application's and leaves the REST of the
# standard order in place, current directory included — so the same host
# run from its own bin\ loads the same stripped module through 0x0008, and
# measuring with an unconstrained CWD measures the CWD.
echo " These resolve from the HOST PROCESS's own directory, so the" \
"host must load this module with a search mode that still includes" \
"it — LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR|LOAD_LIBRARY_SEARCH_DEFAULT_DIRS," \
"or the default order. LOAD_WITH_ALTERED_SEARCH_PATH, and" \
"LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR on its own, both REPLACE that" \
"directory and make a stripped module fail with ERROR_MOD_NOT_FOUND."
"host must load this module with a search mode that includes both" \
"that directory and the module's own:" \
"LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR|LOAD_LIBRARY_SEARCH_DEFAULT_DIRS." \
"Measured on Windows 11 against a real Logos module, every other mode" \
"tried fails with ERROR_MOD_NOT_FOUND — LOAD_WITH_ALTERED_SEARCH_PATH" \
"and DLL_LOAD_DIR-alone because they drop the host's directory, and" \
"the DEFAULT order because it never searches the module's own." \
"(0x0008 does still search the CURRENT directory, so a host started" \
"from its own bin\\ loads it even then — a property of how the host is" \
"launched, not of the flags.)"
elif [ -n "${host_patterns[*]+x}" ]; then
# hostLibs was set and matched NOTHING — neither a staged file nor an
# import. Not an error: one list is shared across many packages and most
@@ -3886,16 +3974,16 @@ if [ "$IS_WINDOWS" = "1" ]; then
# a wrong-namespace list looks like (Unix globs on a PE bundle: `libz*`
# against `zlib1.dll`), and that has to be visible rather than read as "the
# strip ran".
if [ "$pe_host_payload_total" -gt 0 ]; then
if [ "$pe_host_declared_total" -gt 0 ]; then
# A different situation with the same empty ledger, and it must not read
# as the wrong-spelling one: the patterns DID match, on files this
# derivation produced, and the payload rule kept every one of them.
echo " hostLibs matched $pe_host_payload_total file(s) that this" \
"derivation's own build produced, and kept all of them — see the" \
"\`=\` lines in the strip above. Nothing was dropped and nothing" \
"was assigned to the host, so there is no claim to verify. A" \
"hostLibs entry that matches the package's own payload is a caller" \
"mistake even though it is now harmless."
# as the wrong-spelling one: the patterns DID match, inside directories
# the caller named in extraDirs, and every one of those files was kept.
echo " hostLibs matched $pe_host_declared_total file(s) inside a" \
"directory this bundle carries by the caller's own extraDirs, and" \
"kept all of them — see the \`=\` lines in the strip above. Nothing" \
"was dropped and nothing was assigned to the host, so there is no" \
"claim to verify. If those copies were meant to go, take the" \
"directory out of extraDirs; extraDirs is what says \"carry this\"."
else
echo " hostLibs was declared but matched nothing in this bundle: no file" \
"was dropped and no import was assigned to the host. If that is a" \
+46 -19
View File
@@ -27,6 +27,9 @@
let peHostLibs = import ./tests/pe-hostlibs.nix {
inherit pkgs;
mkBundle = import ./mkBundle.nix { inherit pkgs; };
# One subject is about what `qtPlugin` INJECTS, so it has to run
# the real bundler and not a reconstruction of it.
qtPluginBundler = self.bundlers.${system}.qtPlugin;
};
in {
extra-dirs-nested =
@@ -46,14 +49,15 @@
pe-hostlibs-module = peHostLibs.moduleStripped;
pe-hostlibs-module-verified = peHostLibs.moduleStrippedVerified;
pe-hostlibs-claimed-only = peHostLibs.moduleClaimedOnly;
pe-hostlibs-app = peHostLibs.appStripped;
pe-hostlibs-mixed-case = peHostLibs.moduleStrippedMixedCase;
# The payload rule: what the strip may NOT remove.
pe-hostlibs-payload-wildcard = peHostLibs.payloadSurvivesWildcard;
pe-hostlibs-payload-copies = peHostLibs.payloadCopiesKept;
pe-hostlibs-payload-extradirs = peHostLibs.extraDirsPayloadKept;
pe-hostlibs-payload-qt = peHostLibs.qtPayloadKept;
pe-hostlibs-payload-qt-unverified = peHostLibs.qtPayloadKeptUnverified;
pe-hostlibs-copies = peHostLibs.copiesStrippedToo;
pe-hostlibs-bin-stripped = peHostLibs.moduleBinStripped;
# The declaration rule: what the strip may NOT remove, and who may
# ask for a removal at all.
pe-hostlibs-extradirs = peHostLibs.extraDirsKept;
pe-hostlibs-qtplugin-inject = peHostLibs.qtPluginBundlerKeepsPayload;
pe-hostlibs-caller-qt = peHostLibs.callerQtStripsOwnQt;
pe-hostlibs-qt-dir = peHostLibs.qtDirHostProvided;
});
# Subjects that MUST fail to build. Kept out of `checks` on purpose:
@@ -66,6 +70,7 @@
let peHostLibs = import ./tests/pe-hostlibs.nix {
inherit pkgs;
mkBundle = import ./mkBundle.nix { inherit pkgs; };
qtPluginBundler = self.bundlers.${system}.qtPlugin;
};
in {
extra-dirs-dirty =
@@ -80,8 +85,11 @@
pe-hostlibs-host-no-libs = peHostLibs.hostBundleWithoutHostLibs;
pe-hostlibs-host-no-libs-garbage = peHostLibs.hostBundleGarbageIgnored;
pe-hostlibs-host-on-unix = peHostLibs.hostBundleOnUnix;
pe-hostlibs-strip-everything = peHostLibs.stripEverythingLinked;
pe-hostlibs-qt-not-host = peHostLibs.qtPayloadNotHostProvided;
pe-hostlibs-host-unknown = peHostLibs.hostBundleOnUnknownTarget;
pe-hostlibs-host-not-dir = peHostLibs.hostBundleNotADirectory;
pe-hostlibs-strip-everything = peHostLibs.stripEverything;
pe-hostlibs-qt-dir-not-host = peHostLibs.qtDirNotHostProvided;
pe-hostlibs-app-refused = peHostLibs.appShapeRefused;
pe-hostlibs-unclaimed = peHostLibs.moduleUnclaimed;
});
@@ -133,15 +141,32 @@
# A Qt plugin is loaded INTO a Qt host, so the Qt runtime is the
# host's, and this bundler says so on the caller's behalf.
#
# Note what that appended `Qt*` is: a pattern the BUNDLER writes,
# which the caller never sees and cannot review. On the PE path the
# match folds case on both sides, so it reads `qt*` and matches the
# plugin's own `qtquick2plugin.dll` as readily as `Qt6Core.dll`
# measured on the real Windows Qt bundle, before the payload rule
# existed, as 816 PE files in and 796 out with exit 0. It is only
# sound to inject a pattern here because `pe_strip_host_libs` can no
# longer remove anything the bundled derivation's own build produced.
# Do not widen this without re-reading that rule.
# THE INJECTION IS GATED TO NON-WINDOWS TARGETS, and that gate is the
# whole point of this comment. `Qt*` here is a pattern the BUNDLER
# writes: the caller never sees it and cannot review it. What that
# pattern MEANS is not the same on both platforms
#
# * on ELF/Mach-O `hostLibs` filters what `trace_deps` ADDS. The
# worst an injected pattern can do is decline to copy something
# in, which is exactly what this bundler is for.
# * on PE it DELETES, because win-dll-link.sh has already staged the
# import closure into the derivation's own output. The PE match
# also folds case on both sides, so `Qt*` reads as `qt*` and
# matches the plugin's own `qtquick2plugin.dll` as readily as
# `Qt6Core.dll`. Measured on the real Windows Qt bundle while this
# injection still reached the PE path: 816 PE files in, 796 out,
# exit 0 — the bundler silently deleting what it was asked to
# package.
#
# So the rule is: a bundler-injected pattern must never be able to
# delete. A caller who wants a Qt plugin's Qt runtime stripped on
# Windows writes `hostLibs = [ "Qt*.dll" ]` themselves, and gets
# exactly that, with `hostBundle` to check it.
#
# `or false` is right for a drv with no stdenv: bundle.sh resolves an
# unknown target to Unix (Phase 1c), so those bundles take the path
# where the injection is meaningful and harmless — and this expression
# answers the same question bundle.sh will.
qtPlugin = drv:
mkBundle {
inherit drv;
@@ -149,7 +174,9 @@
extraDirs = drv.extraDirs or [];
extraClosurePaths = drv.extraClosurePaths or [];
hostBundle = drv.hostBundle or null;
hostLibs = (drv.hostLibs or []) ++ [ "Qt*" ];
hostLibs = (drv.hostLibs or [])
++ nixpkgs.lib.optional
(!(drv.stdenv.hostPlatform.isWindows or false)) "Qt*";
warnOnBinaryData = true;
};
});
+53 -18
View File
@@ -26,15 +26,29 @@
# spellings do under-match (`libz*` cannot match `zlib1.dll`), but nothing in
# this design may rest on that.
#
# What keeps a wrong list from being destructive is not the shape of the glob:
# WHAT THE PE PATH MAY DELETE. On ELF and Mach-O this list only filters what
# `trace_deps` copies IN; on Windows the import closure is already inside the
# derivation's output (nixpkgs' win-dll-link.sh stages it during fixup), so the
# bundler has to delete, and deleting is the more dangerous operation. It is
# bounded by WHO DECLARED WHAT, and infers nothing about a file:
#
# * on the PE path the strip may only remove a file that came along WITH the
# package — one the bundler staged, or one win-dll-link.sh linked in from
# another store path — and never a file the derivation's own build
# produced (the payload rule; see `pe_strip_host_libs` in bundle.sh);
# * `hostBundle` below turns every remaining claim into a checked fact;
# * and bundle.sh says so in the log when a declared list matches nothing at
# all, which is what a wrong-namespace list looks like.
# * a pattern the CALLER wrote deletes. `hostLibs = [ "Qt*" ]` on a Qt plugin
# package removes that package's own Qt6*.dll, and that is the contract;
# * a pattern a BUNDLER injects on the caller's behalf never reaches the PE
# path at all — see `bundlers.<sys>.qtPlugin` in flake.nix, which appends
# `Qt*` only on a non-Windows target;
# * `extraDirs` is never touched. Those directories are named by the caller,
# one by one, as "carry this";
# * and what does go is checked (`hostBundle`) or listed by name in the log
# as UNVERIFIED, so an over-broad list is loud rather than silent.
#
# An earlier revision bounded the strip by PROVENANCE instead — a regular file
# in the derivation's output is its own build product, a symlink is a dependency
# travelling with it — which is recorded because it reads well and does not
# work: in the real `logos-package_manager-module` Windows lib/, 8 of 19 DLLs
# (icudt76, icuuc76, libstdc++-6, libgcc_s_seh-1, libmcfgthread-2, libsodium-26
# and more) are REGULAR FILES, so on the shape this feature exists for it kept
# everything and stripped nothing.
#
# See the long note above `pe_is_host_lib` in bundle.sh for why the LIST is
# shared and only the MATCH is platform-specific.
@@ -62,18 +76,39 @@
#
# What it CANNOT check is the other half of the promise: that the host will
# look in its own directory when it loads this module. Measured on Windows 11
# x86-64, a stripped module against a host that does ship the DLLs:
# x86-64 against the REAL logos-package_manager module (16 DLLs stripped, 3
# kept) and a host bundle shipping those 16, with the UNSTRIPPED bundle of the
# same module as the control and the process's CURRENT DIRECTORY holding none
# of them:
#
# LoadLibraryEx flags 0x0000 (default order) LOADS
# LoadLibraryEx flags 0x0008 ALTERED_SEARCH_PATH fails, 126
# LoadLibraryEx flags 0x0100 SEARCH_DLL_LOAD_DIR fails, 126
# LoadLibraryEx flags 0x1100 DLL_LOAD_DIR|DEFAULT_DIRS LOADS
# stripped control
# flags 0x1100 DLL_LOAD_DIR|DEFAULT_DIRS LOADS LOADS
# flags 0x0008 ALTERED_SEARCH_PATH 126 LOADS <- the strip
# flags 0x0008, CWD = the host's bin\ LOADS -
# flags 0x0000 (default order) 126 126 <- not the strip
# flags 0x0100 SEARCH_DLL_LOAD_DIR alone 126 126 <- not the strip
# flags 0x1000 SEARCH_DEFAULT_DIRS alone 126 126 <- not the strip
# flags 0x1100, host missing one claim 126 LOADS <- the claim
#
# with an unstripped control loading in every row, so the failures are the
# strip and not the machine. The middle two REPLACE the application directory
# with the module's own instead of adding to it. logos-module passes 0x1100
# (src/win_dll_search.h). A host that passes one of the other two cannot host a
# stripped bundle, and no build-time check can see which one it will use.
# logos-module passes 0x1100 (src/win_dll_search.h), which is the row that
# matters. Only two rows are attributable to the strip at all — the ones where
# the control differs — and reading the others as flag verdicts is how the
# previous version of this table got 0x0000 wrong: it was measured on a
# synthetic module with no private dependencies, and a real module keeps its own
# DLLs beside it, which the default order never searches.
#
# The CWD qualifier is load-bearing too: 0x0008 substitutes the module's
# directory for the application's and leaves the rest of the standard order —
# CURRENT DIRECTORY included — in place, so the same host launched from its own
# bin\ loads the same stripped module through 0x0008.
#
# And it only applies to a MODULE. A bundle with an executable of its own is
# refused by bundle.sh the moment hostLibs would drop anything, because for that
# shape the loading process is the bundle's own .exe and the directory Windows
# searches is the bundle's own bin\ — the host's DLLs are not there, and this
# argument would be verifying a directory the loader never consults. Measured:
# such a bundle run from its own bin\ dies 0xC0000135 with no output, and runs
# only with the host's bin\ on PATH.
#
# Not consulted on ELF/Mach-O, and refused there rather than silently dropped —
# see the `throwIf` below. Those paths are unchanged by this argument, down to
+179 -119
View File
@@ -13,24 +13,28 @@
# 3. the ACCEPTANCE, in Phase 6, without which the verifier fails the build
# over the very DLLs the strip was told not to carry.
#
# And one rule that BOUNDS the strip, because deleting is not the same
# And the rule that BOUNDS the strip, because deleting is not the same
# operation as declining to add. On ELF and Mach-O `hostLibs` only ever filters
# what `trace_deps` copies IN; on Windows the import closure has already been
# staged into the derivation's own output by nixpkgs' win-dll-link.sh before
# the bundler runs, so the PE path has to delete. The rule is:
# the bundler runs, so the PE path has to delete. The rule is about
# DECLARATION, and infers nothing about any file:
#
# the strip may only remove a file that came along WITH the package — one
# the bundler staged, or one win-dll-link.sh linked in from another store
# path — never a file the derivation's own build produced.
# * a pattern the CALLER wrote deletes — including one that matches the
# package's own build products;
# * a pattern a BUNDLER injects never reaches the PE path (flake.nix's
# `qtPlugin` appends `Qt*` only on a non-Windows target);
# * a directory named in `extraDirs` is never touched.
#
# That rule is why the fixtures below distinguish LINKED runtime DLLs (the real
# win-dll-link shape, `ln -sr` into another store path) from COPIED ones. Both
# shapes exist in the wild and the bundler must treat them differently: the
# first is a dependency travelling with the package, the second is the
# package's own output. Two measured defects — `bundlers.qtPlugin` deleting
# `qtquick2plugin.dll` because it appends `Qt*` to hostLibs itself, and the
# strip emptying an `extraDirs` entry — close on that one rule, and neither
# closes on a rule about pattern syntax.
# The fixtures below therefore no longer distinguish LINKED from COPIED DLLs as
# a matter of policy — `moduleLinked` and `moduleCopied` are stripped alike, and
# a subject asserts exactly that. An earlier revision bounded the strip by that
# distinction (regular file = the build produced it, symlink = a dependency
# travelling with it) and it fails on the shape that matters: in the real
# `logos-package_manager-module` x86_64-w64-mingw32 output, 8 of the 19 DLLs in
# lib/ — icudt76, icuuc76, libstdc++-6, libgcc_s_seh-1, libmcfgthread-2,
# libsodium-26, zlib1 and more — are REGULAR FILES, so the rule called the whole
# host runtime payload and stripped nothing.
#
# The fourth part is `hostBundle`, which makes the list checkable rather than
# declared. Every hostLibs entry is a promise about another repo's output, and
@@ -46,10 +50,13 @@
#
# x86_64-linux only, by the caller's gate in flake.nix: every subject is a
# `pkgsCross.mingwW64` build, and the mingw toolchain is substitutable there.
{ pkgs, mkBundle }:
#
# `qtPluginBundler` is flake.nix's own `bundlers.<sys>.qtPlugin`, passed in
# because one subject is about what THAT function injects, which cannot be
# reproduced by calling mkBundle directly.
{ pkgs, mkBundle, qtPluginBundler }:
let
inherit (pkgs) lib;
mingw = pkgs.pkgsCross.mingwW64;
# The C++ runtime DLLs a mingw C++ module imports. Chosen because they are
@@ -86,10 +93,7 @@ let
# `dontFixup`, so each fixture is exactly what this file says it is: the
# mingw stdenv's own win-dll-link hook would otherwise stage DLLs on its own
# schedule and the subjects would be asserting about its output rather than
# about the bundler's. The hook's SHAPE is reproduced by hand where it
# matters — `ln -s` into another store path, which is what `ln -sr` produces
# once Nix has resolved it — because that shape is exactly what the payload
# rule reads.
# about the bundler's.
# SHAPE 1: a module. `lib/<name>_plugin.dll` and NOTHING else -- no bin/.
# This is the shape nix-bundle-lgx feeds through the bundler, and the one
@@ -118,10 +122,10 @@ let
'';
};
# SHAPE 1c: the same names, but COPIED into the derivation's own output
# rather than linked. Byte-for-byte the same DLLs; the difference is that
# this derivation produced them, so they are its payload and the strip may
# not touch them however the patterns read.
# SHAPE 1c: the same names, COPIED into the derivation's own output rather
# than linked -- which is what the real module output looks like for most of
# its runtime DLLs. Byte-for-byte the same DLLs; under the declaration rule
# the bundler treats them identically, and `copiesStrippedToo` asserts it.
moduleCopied = mingw.stdenv.mkDerivation {
name = "hostlibs-module-copied";
dontUnpack = true;
@@ -137,9 +141,7 @@ let
# SHAPE 1d: a module carrying a DLL in a directory the caller asked for by
# name. `extraDirs` is an explicit "carry this", and the strip walked the
# whole of $out with no exception for it -- measured: the entry shipped
# EMPTY, rc=0. Not fixable by exempting extraDirs wholesale either, since the
# sweep legitimately stages a dependency into an extraDir when the importer
# lives there; only provenance separates the two.
# EMPTY, rc=0.
moduleWithAssets = mingw.stdenv.mkDerivation {
name = "hostlibs-module-assets";
dontUnpack = true;
@@ -154,26 +156,60 @@ let
'';
};
# SHAPE 1e: a derivation with NO output of its own -- every file in it is a
# link to another store path. By the payload rule it has no payload, so
# `hostLibs` governs its whole tree and an over-broad list can still empty
# it. That is the one shape the floor in pe_strip_host_libs still guards, and
# this is what makes the floor demonstrable rather than asserted.
moduleAllLinked = mingw.stdenv.mkDerivation {
name = "hostlibs-module-all-linked";
# SHAPE 1e: a library package's bin/ -- DLLs and no executable, which is what
# `linkDLLsInfolder "$out/bin"` leaves behind. Nothing in it can be a process,
# so the app-shape refusal does not apply, and it is the shape that reaches
# Phase 6's "this bin/ entry is missing because THIS build removed it" arm.
moduleBinDlls = mingw.stdenv.mkDerivation {
name = "hostlibs-module-bin-dlls";
dontUnpack = true;
dontBuild = true;
dontFixup = true;
buildPhase = "$CXX -shared -o demo_plugin.dll ${pluginSrc}";
installPhase = ''
mkdir -p $out/lib
mkdir -p $out/lib $out/bin && cp demo_plugin.dll $out/lib/
ln -s ${dllDir}/libstdc++-6.dll ${dllDir}/libgcc_s_seh-1.dll ${mcfgDll} $out/bin/
'';
};
# SHAPE 1f: a module whose OWN BUILD produces a PE named like a Qt library --
# the shape of every Qt plugin package. Used twice, for the two halves of the
# declaration rule: a CALLER's `Qt*.dll` removes this Qt6Core.dll, and the
# `qtPlugin` bundler's injected `Qt*` does not.
qtNamedModule = mingw.stdenv.mkDerivation {
name = "hostlibs-qt-named-module";
dontUnpack = true;
dontFixup = true;
buildPhase = ''
$CXX -shared -o demo_plugin.dll ${pluginSrc}
$CXX -shared -o Qt6Core.dll ${pluginSrc}
'';
installPhase = ''
mkdir -p $out/lib && cp demo_plugin.dll Qt6Core.dll $out/lib/
ln -s ${dllDir}/libstdc++-6.dll ${dllDir}/libgcc_s_seh-1.dll ${mcfgDll} $out/lib/
'';
};
# SHAPE 2: an application. Has a bin/, which is a different code path in
# three places -- the sweep stages into it, Phase 1c counts it, and Phase 6
# asserts every source bin/ entry survived. That last one FAILS the build on
# a stripped app unless it is taught the same rule as the strip.
# SHAPE 1g: a bin/ holding a DIRECTORY named like a Qt DLL. Contrived-looking
# and not invented: it is the one shape that still reaches the
# `pe_is_host_lib "$qt_lib_name"` arm in Qt detection, because the strip walks
# `-type f` and cannot remove a directory while the detection loop's `[ -e ]`
# counts one. bundle.sh has carried three different wrong claims about that
# arm's reachability; this is what makes the current one checkable.
qtDirModule = mingw.stdenv.mkDerivation {
name = "hostlibs-qt-dir-module";
dontUnpack = true;
dontFixup = true;
buildPhase = "$CXX -shared -o demo_plugin.dll ${pluginSrc}";
installPhase = ''
mkdir -p $out/lib $out/bin/Qt6Core.dll
cp demo_plugin.dll $out/lib/
ln -s ${dllDir}/libstdc++-6.dll ${dllDir}/libgcc_s_seh-1.dll ${mcfgDll} $out/lib/
'';
};
# SHAPE 2: an application. It has an executable of its own, so it can BE the
# process -- and then the directory Windows searches is this bundle's bin/,
# not the host's. Every hostLibs claim on this shape is refused.
appLinked = mingw.stdenv.mkDerivation {
name = "hostlibs-app-linked";
dontUnpack = true;
@@ -194,25 +230,6 @@ let
installPhase = "mkdir -p $out/bin && cp demo_app.exe $out/bin/";
};
# SHAPE 3: an application whose OWN BUILD produces a PE named like a Qt
# library. Contrived-looking and not contrived: it is the shape of every Qt
# module package, and it is the only way to reach the `pe_is_host_lib
# "$qt_lib_name"` arm in the Qt-detection block with a host-claimed name --
# which an earlier comment in bundle.sh asserted was unreachable.
qtNamedApp = mingw.stdenv.mkDerivation {
name = "hostlibs-qt-named-app";
dontUnpack = true;
dontFixup = true;
buildPhase = ''
$CXX -o demo_app.exe ${appSrc}
$CXX -shared -o Qt6Core.dll ${pluginSrc}
'';
installPhase = ''
mkdir -p $out/bin && cp demo_app.exe Qt6Core.dll $out/bin/
ln -s ${dllDir}/libstdc++-6.dll ${dllDir}/libgcc_s_seh-1.dll ${mcfgDll} $out/bin/
'';
};
# The host's own bundle: the tree whose application directory the host
# process runs from. `bin/`, because that is the directory Windows searches
# for the loading process.
@@ -222,6 +239,15 @@ let
cp ${appPlain}/bin/demo_app.exe $out/bin/host.exe
'';
# The same host, plus a Qt6Core.dll -- for the subject where the CALLER
# declares `Qt*.dll` and the bundle's own Qt-named DLL is therefore dropped.
hostWithQt = pkgs.runCommand "hostlibs-host-qt" { } ''
mkdir -p $out/bin
cp ${dllDir}/libstdc++-6.dll ${dllDir}/libgcc_s_seh-1.dll ${mcfgDll} $out/bin/
cp ${qtNamedModule}/lib/Qt6Core.dll $out/bin/
cp ${appPlain}/bin/demo_app.exe $out/bin/host.exe
'';
# The same host with libgcc_s_seh-1.dll in lib/ instead of bin/. Not a
# contrived shape: "it is in the host bundle somewhere" is precisely the
# mistake, and it is invisible to any check that greps the whole tree.
@@ -245,6 +271,8 @@ let
# `find | sort` of the bundle's PE files, relative -- the whole assertion is
# about WHICH files are there, so compare the set rather than a count.
# Deliberately not `-type f`: a DIRECTORY named like a DLL is part of what
# some subjects are asserting about.
peSet = b: "cd ${b} && find . -name '*.dll' -o -name '*.exe' | LC_ALL=C sort | tr '\\n' ' '";
expect = name: bundle: want: pkgs.runCommand "check-${name}" { } ''
@@ -292,17 +320,6 @@ in
})
"./lib/demo_plugin.dll ";
# The app shape. bin/ loses two DLLs, which Phase 6's "every source bin/
# entry is still here" check has to accept as deliberate -- and it accepts
# them by PATH, from the set this build actually removed, not by re-testing
# the name against hostLibs.
appStripped = expect "app-stripped"
(bundleOf {
drv = appLinked; name = "hostlibs-app-stripped";
hostLibs = hostDlls; hostBundle = hostGood;
})
"./bin/demo_app.exe ";
# A PE import table spells one DLL two ways inside a single bundle
# (KERNEL32.DLL and KERNEL32.dll both occur in this toolchain's own output),
# so the PE match folds case on BOTH sides -- pattern included. A caller
@@ -315,35 +332,36 @@ in
})
"./lib/demo_plugin.dll ";
# ---- the payload rule ---------------------------------------------------
# THE `*.dll` SUBJECT. Before the payload rule this pattern removed every PE
# in the bundle and the build died on the floor; the real defect it stands
# for is the same pattern removing SOME payload and exiting 0. Now the
# patterns cannot reach the derivation's own output at all, so the widest
# possible list still ships the package.
payloadSurvivesWildcard = expect "payload-survives-wildcard"
# COPIED, not linked. Identical outcome to `moduleStripped`, and that is the
# assertion: how a file got into the derivation's output is not something this
# bundler reasons about. The provenance rule this replaces kept all three of
# these and reported a successful strip of nothing.
copiesStrippedToo = expect "copies-stripped-too"
(bundleOf {
drv = moduleLinked; name = "hostlibs-payload-wildcard"; hostLibs = [ "*.dll" ];
drv = moduleCopied; name = "hostlibs-copies";
hostLibs = hostDlls; hostBundle = hostGood;
})
"./lib/demo_plugin.dll ";
# The same names, copied rather than linked: this derivation produced them,
# so all four files stay. hostBundle is given, which before the fix would
# have failed the build -- the import arms claimed every matching name from
# the host whether or not the bundle already satisfied it.
payloadCopiesKept = expect "payload-copies-kept"
# A bin/ with no executable in it: the strip empties it, and Phase 6 has to
# tell "this build removed bin/x.dll" from "Phase 1 lost bin/x.dll". Before
# that arm existed this build failed with three "in the source derivation but
# not in the bundle" errors -- the strip and the verifier disagreeing about
# one decision.
moduleBinStripped = expect "module-bin-stripped"
(bundleOf {
drv = moduleCopied; name = "hostlibs-payload-copies";
drv = moduleBinDlls; name = "hostlibs-module-bin";
hostLibs = hostDlls; hostBundle = hostGood;
})
"./lib/demo_plugin.dll ./lib/libgcc_s_seh-1.dll ./lib/libmcfgthread-2.dll ./lib/libstdc++-6.dll ";
"./lib/demo_plugin.dll ";
# ---- what the strip may NOT remove --------------------------------------
# `extraDirs` is an explicit "carry this". The linked-in copies in lib/ go;
# the caller's own copy under share/assets stays. Measured before the fix:
# "- share/assets/libstdc++-6.dll (host-provided)", the directory shipped
# empty, rc=0.
extraDirsPayloadKept = expect "extra-dirs-payload-kept"
extraDirsKept = expect "extra-dirs-kept"
(bundleOf {
drv = moduleWithAssets; name = "hostlibs-assets";
hostLibs = hostDlls; hostBundle = hostGood;
@@ -351,31 +369,41 @@ in
})
"./lib/demo_plugin.dll ./share/assets/libstdc++-6.dll ";
# `bundlers.qtPlugin` appends `Qt*` to hostLibs ITSELF, and the PE match
# folds case, so it reads `qt*` and matches a Qt plugin's own
# `qtquick2plugin.dll`. Here the derivation's own Qt6Core.dll survives a
# `Qt*.dll` list while the linked-in runtime DLLs are stripped -- and the
# build only gets that far because the survivor makes the Qt-detection arm
# answer "host-provided", which is what `qtPayloadNotHostProvided` below
# controls for.
qtPayloadKept = expect "qt-payload-kept"
# THE BUNDLER-INJECTED PATTERN. `bundlers.<sys>.qtPlugin` appends `Qt*` to
# hostLibs itself -- a list the caller never sees -- and on the PE path that
# would DELETE, case-folded, matching a Qt plugin package's own output.
# Measured on the real Windows Qt bundle while it did: 816 PEs in, 796 out,
# exit 0. flake.nix now appends it only on a non-Windows target, so this
# bundle keeps its own Qt6Core.dll and everything else it was given.
qtPluginBundlerKeepsPayload = expect "qt-plugin-bundler-keeps-payload"
(qtPluginBundler qtNamedModule)
("./lib/Qt6Core.dll ./lib/demo_plugin.dll ./lib/libgcc_s_seh-1.dll "
+ "./lib/libmcfgthread-2.dll ./lib/libstdc++-6.dll ");
# The other half of the same rule, and the reason the gate is about WHO
# declared the pattern rather than about the pattern: the same file name, the
# same bundle, a CALLER-written `Qt*.dll` -- and it goes, because that is what
# the caller asked for. hostBundle then has to ship it, which is what makes
# the request checkable rather than merely obeyed.
callerQtStripsOwnQt = expect "caller-qt-strips-own-qt"
(bundleOf {
drv = qtNamedApp; name = "hostlibs-qt-payload";
drv = qtNamedModule; name = "hostlibs-caller-qt";
hostLibs = hostDlls ++ [ "Qt*.dll" ]; hostBundle = hostWithQt;
})
"./lib/demo_plugin.dll ";
# The `pe_is_host_lib "$qt_lib_name"` arm in Qt detection, reached the one way
# it still can be: a DIRECTORY named like a Qt DLL, which the strip cannot
# remove (`find -type f`) and detection counts (`[ -e ]`). It answers
# "host-provided", so Phase 2b skips Qt staging and this builds. The control
# is `qtDirNotHostProvided` below, which is the same input without `Qt*` and
# dies demanding a Qt plugin directory.
qtDirHostProvided = expect "qt-dir-host-provided"
(bundleOf {
drv = qtDirModule; name = "hostlibs-qt-dir";
hostLibs = hostDlls ++ [ "Qt*.dll" ]; hostBundle = hostGood;
})
"./bin/Qt6Core.dll ./bin/demo_app.exe ";
# The same subject with NO hostBundle, which is the defect exactly as it
# shipped: `bundlers.qtPlugin` appends `Qt*` and passes no host bundle, so
# nothing adjudicated the claim and the deletion was silent. Measured against
# the pre-fix tree: exit 0 with a PE set of `./bin/demo_app.exe ` — the
# bundler had quietly removed the file it was packaging.
qtPayloadKeptUnverified = expect "qt-payload-kept-unverified"
(bundleOf {
drv = qtNamedApp; name = "hostlibs-qt-payload-u";
hostLibs = hostDlls ++ [ "Qt*" ];
})
"./bin/Qt6Core.dll ./bin/demo_app.exe ";
"./bin/Qt6Core.dll ./lib/demo_plugin.dll ";
# ---- must FAIL, each on a different arm --------------------------------
# Kept out of `checks` for the same reason extra-dirs-dirty is: `nix flake
@@ -421,20 +449,52 @@ in
hostBundle = hostGood;
};
# The floor, on the one shape that can still reach it: a derivation that
# produced no file of its own, so the payload rule has nothing to protect.
stripEverythingLinked = bundleOf {
drv = moduleAllLinked; name = "hostlibs-fail-stripall"; hostLibs = [ "*.dll" ];
# hostBundle on a target Nix cannot read. mkBundle's throwIf deliberately
# fires on `isWindowsStr == "0"` only, leaving "unknown" to bundle.sh, which
# resolves it to Unix in Phase 1c and refuses there -- an arm nothing reached
# until this subject, because every other stdenv-less shape also has no
# reason to pass a hostBundle. `removeAttrs` is how a real Windows tree with
# no stdenv is reproduced: a `builtins.storePath` cannot be written in a pure
# flake, and everything else about the derivation is unchanged.
hostBundleOnUnknownTarget = bundleOf {
drv = builtins.removeAttrs moduleLinked [ "stdenv" ];
name = "hostlibs-fail-unknown"; hostLibs = hostDlls; hostBundle = hostGood;
};
# The control for `qtPayloadKept`: identical input, no `Qt*` in hostLibs. Qt
# is then detected and NOT host-provided, so Phase 2b goes looking for a Qt
# hostBundle that is not a directory at all. `pe_host_index` refuses it, and
# nothing reached that arm either: every other subject passes a runCommand
# output. A `.cpp` in the store is a store path a caller could plausibly
# paste in.
hostBundleNotADirectory = bundleOf {
drv = moduleLinked; name = "hostlibs-fail-notdir";
hostLibs = hostDlls; hostBundle = pluginSrc;
};
# The floor. `*.dll` is a list the caller is ALLOWED to write -- a declared
# pattern deletes -- but a bundle with no PE left in it cannot be what anyone
# meant, and it is the one case that needs no judgement about which file was
# the payload. Under the provenance rule this same subject built green.
stripEverything = bundleOf {
drv = moduleLinked; name = "hostlibs-fail-stripall"; hostLibs = [ "*.dll" ];
};
# The control for `qtDirHostProvided`: identical input, no `Qt*` in hostLibs.
# Qt is then detected and NOT host-provided, so Phase 2b goes looking for a Qt
# plugin directory in a closure that has none and the build dies. That
# difference is the demonstration that the `pe_is_host_lib "$qt_lib_name"`
# arm is reached and changes the verdict -- bundle.sh used to carry a comment
# claiming it could not be.
qtPayloadNotHostProvided = bundleOf {
drv = qtNamedApp; name = "hostlibs-fail-qt-not-host";
# difference is the demonstration that the arm is reached and changes the
# verdict.
qtDirNotHostProvided = bundleOf {
drv = qtDirModule; name = "hostlibs-fail-qt-dir";
hostLibs = hostDlls; hostBundle = hostGood;
};
# The app shape. Windows resolves a host-provided DLL through the LOADING
# PROCESS's own directory; when the bundle has an executable of its own that
# directory is the bundle's bin/, where the stripped DLLs no longer are.
# Measured: such a bundle dies 0xC0000135 before main() with no output, and
# runs only with the host's bin/ on PATH. It used to build green here.
appShapeRefused = bundleOf {
drv = appLinked; name = "hostlibs-fail-app";
hostLibs = hostDlls; hostBundle = hostGood;
};
+12 -6
View File
@@ -93,7 +93,7 @@ echo
#
# This block exists because the contract in tests/pe-hostlibs.nix was reachable
# only through `nix flake check`, and CI does not run `nix flake check` — it
# runs this file. Ten subjects that must build and eight that must be refused
# runs this file. Ten subjects that must build and eleven that must be refused
# were therefore built by nobody, which is the same as not having them.
#
# x86_64-linux only, and by SYSTEM rather than by `uname`: every subject is a
@@ -106,9 +106,9 @@ if [ "$SYS" = "x86_64-linux" ]; then
# Must BUILD. Each asserts the resulting PE SET, not a count: the defect
# class here is a bundle that is missing a file and exits 0.
for c in pe-hostlibs-module pe-hostlibs-module-verified pe-hostlibs-claimed-only \
pe-hostlibs-app pe-hostlibs-mixed-case pe-hostlibs-payload-wildcard \
pe-hostlibs-payload-copies pe-hostlibs-payload-extradirs \
pe-hostlibs-payload-qt pe-hostlibs-payload-qt-unverified; do
pe-hostlibs-mixed-case pe-hostlibs-copies pe-hostlibs-bin-stripped \
pe-hostlibs-extradirs pe-hostlibs-qtplugin-inject \
pe-hostlibs-caller-qt pe-hostlibs-qt-dir; do
if nix build -L "$FLAKE#checks.$SYS.$c" --no-link > "pe-$c.log" 2>&1; then
ok "$c"
else
@@ -120,7 +120,10 @@ if [ "$SYS" = "x86_64-linux" ]; then
# A subject that fails for the wrong reason is the failure this whole file is
# written against, and exit status alone cannot tell the two apart.
#
# Pairs of "attribute<TAB>expected fragment".
# Pairs of "attribute|expected fragment", split on the `|` this loop's IFS
# actually sets. (It said TAB, over a reader that has never been able to see
# one — the fifth comment on this branch found describing something that is
# not there.)
while IFS='|' read -r c want; do
[ -n "$c" ] || continue
if nix build -L "$FLAKE#tests.$SYS.$c" --no-link > "pe-$c.log" 2>&1; then
@@ -137,8 +140,11 @@ pe-hostlibs-host-vacuous|has no PE file in its
pe-hostlibs-host-no-libs|ERROR: hostBundle was given but hostLibs is empty
pe-hostlibs-host-no-libs-garbage|has no PE file in its
pe-hostlibs-host-on-unix|whose target Nix reports as
pe-hostlibs-host-unknown|ERROR: hostBundle was given, but this bundle's target is not Windows
pe-hostlibs-host-not-dir|which is not a directory
pe-hostlibs-strip-everything|ERROR: the hostLibs strip removed every PE in this bundle
pe-hostlibs-qt-not-host|no Qt plugin directory for this
pe-hostlibs-qt-dir-not-host|no Qt plugin directory for this
pe-hostlibs-app-refused|but this bundle contains an executable of its own
pe-hostlibs-unclaimed|DLL import(s) could not be resolved
PE_FAILS
else