Skip to content

Commit c49cbb6

Browse files
committed
fix(dist-wix): no double hyphen in the bundle document's comment, and a plan-level check that parses both documents (mcpp#634)
wix build refused the generated bundle definition (windows-2022, run 34821164486, job 103902829448): "error WIX0104: Not a valid source file; detail: An XML comment cannot contain '--'", because the comment named the command line's --format. The comment no longer does, and no longer names a preprocessor variable either. tests/msi-consumer/check-wix-plan.sh runs the build program for a Windows target on any host, with a fabricated xim:wix directory: the bundle action follows the MSI and is the sole terminal artifact, both generated documents parse as XML (Python's parser refuses the previous document at line 4, column 40), --format msi plans no bundle, and a bundle named setup.exe is refused as a warning. The fixture's build program reads MSI_CONSUMER_BUNDLE_OUTPUT for that last case. The consumers job runs it.
1 parent 44f3a72 commit c49cbb6

4 files changed

Lines changed: 138 additions & 3 deletions

File tree

.github/workflows/ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,15 @@ jobs:
11851185
working-directory: tests/app-framework-consumer
11861186
run: MCPP="$MCPP" ./check-apple-plan.sh
11871187

1188+
# dist-wix's `setup` format, as planned for a Windows target (mcpp#634,
1189+
# B4): the bundle follows the MSI, both generated documents are
1190+
# well-formed XML, and `setup.exe` is refused. The bundle itself is built
1191+
# on the Windows runner.
1192+
- name: dist-wix's MSI and Burn bundle, checked at the plan level
1193+
timeout-minutes: 15
1194+
working-directory: tests/msi-consumer
1195+
run: MCPP="$MCPP" ./check-wix-plan.sh
1196+
11881197
# dist-web (#622 B3), end to end, the real `mcpp pack --format web
11891198
# --target wasm32-emscripten` -- see `tests/web-consumer/
11901199
# check-web-plan.sh`'s own header for the host-toolchain defect this

dist/wix.cppm

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -481,16 +481,22 @@ inline std::string discover_bal_extension(const options& opt) {
481481
// two substitution passes). The bundle carries its own UpgradeCode, derived
482482
// from the product's identity and distinct from the MSI's: a bundle and the
483483
// package it installs are two products to Windows.
484+
//
485+
// NO TEXT IN THE DOCUMENT MAY HOLD TWO HYPHENS IN A ROW OUTSIDE AN ATTRIBUTE.
486+
// XML forbids `--` inside a comment, and `wix build` refused this document
487+
// when its comment named the command line's `--format` (WIX0104, measured on
488+
// windows-2022); `tests/msi-consumer/check-wix-plan.sh` parses both generated
489+
// documents on every host.
484490
inline std::string bundle_document(const std::string& name, const std::string& manufacturer,
485491
const std::string& version, const std::string& upgrade_code,
486492
const std::string& license_url) {
487493
return std::format(
488494
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
489495
"<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\"\n"
490496
" xmlns:bal=\"http://wixtoolset.org/schemas/v4/wxs/bal\">\n"
491-
" <!-- Generated by mcpp.dist.wix for --format setup. A project with its\n"
492-
" own bootstrapper application supplies options::bundle_wxs, which\n"
493-
" receives the MSI as $(Msi). -->\n"
497+
" <!-- Generated by mcpp.dist.wix for the setup format. A project with\n"
498+
" its own bootstrapper application supplies options::bundle_wxs,\n"
499+
" which receives the path of the MSI as the variable Msi. -->\n"
494500
" <Bundle Name=\"{0}\" Manufacturer=\"{1}\" Version=\"{2}\" "
495501
"UpgradeCode=\"{3}\">\n"
496502
" <BootstrapperApplication>\n"

tests/msi-consumer/build.mcpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ int main() {
66
mcpp::dist::wix::options opt;
77
opt.target = "msi-consumer";
88
opt.product_name = "MsiConsumer";
9+
// `check-wix-plan.sh` names the bundle's file through this variable to
10+
// reach the `setup.exe` refusal without a second fixture.
11+
if (const char* out = std::getenv("MSI_CONSUMER_BUNDLE_OUTPUT"); out && *out)
12+
opt.bundle_output = out;
913
// The version, the manufacturer and the upgrade code all come from
1014
// `[package]`: an installer that restated them would carry a copy that
1115
// drifts from the manifest with nothing able to detect it.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env bash
2+
# Plan-level check for dist-wix's `msi` and `setup` formats (mcpp#634, B4).
3+
#
4+
# The bundle is built on the Windows runner (`check-setup.sh`). This script runs
5+
# on any host: it runs the compiled build program under the environment the
6+
# engine sets for `mcpp pack --format msi` and `--format setup` on a Windows
7+
# target, with a fabricated `xim:wix` directory that holds the two files the
8+
# member looks for, and reads what it plans and writes.
9+
#
10+
# 1. `--format setup` plans the MSI and a bundle action whose inputs include
11+
# the MSI, so the bundle is the only terminal artifact, and whose command
12+
# loads the extension and names the MSI as the variable `Msi`.
13+
# 2. Both generated documents are well-formed XML. `wix build` refused a
14+
# bundle document whose comment held `--` (WIX0104, windows-2022), and
15+
# an XML parser refuses the same document on every host.
16+
# 3. `--format msi` plans no bundle.
17+
# 4. A bundle named `setup.exe` is refused before `wix` runs, as a warning.
18+
#
19+
# Usage: MCPP=<mcpp> ./check-wix-plan.sh (run from this directory)
20+
set -eu
21+
22+
MCPP="${MCPP:-mcpp}"
23+
fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; }
24+
command -v python3 > /dev/null || fail "python3 is required to read the planned actions"
25+
26+
rm -rf target
27+
"$MCPP" build > wix-plan-build.log 2>&1 || fail "the host build failed to compile build.mcpp" wix-plan-build.log
28+
BIN=target/.build-mcpp/build.mcpp.bin
29+
[ -x "$BIN" ] || fail "no compiled build.mcpp.bin at $BIN" wix-plan-build.log
30+
31+
work=$(mktemp -d)
32+
wix="$work/xpkg-wix"
33+
mkdir -p "$wix/tool/tools/net6.0/any" "$wix/bal/wixext5"
34+
: > "$wix/tool/tools/net6.0/any/wix.exe"
35+
: > "$wix/bal/wixext5/WixToolset.BootstrapperApplications.wixext.dll"
36+
37+
# run_program <format> <out dir> <log> [bundle output]
38+
run_program() {
39+
mkdir -p "$2"
40+
env -i PATH="$PATH" \
41+
MCPP_PACK_FORMAT="$1" MCPP_TARGET_OS=windows MCPP_TARGET_ARCH=x86_64 \
42+
MCPP_TARGET_ENV=msvc MCPP_PACK_STAGE_DIR="" MCPP_MANIFEST_DIR="$PWD" \
43+
MCPP_PKG_NAME=msi-consumer MCPP_PKG_NAMESPACE= MCPP_PKG_VERSION=0.3.0 \
44+
MCPP_PKG_AUTHORS=mcpp-community MCPP_OUT_DIR="$2" \
45+
MCPP_XPKG_XIM_WIX_DIR="$wix" MSI_CONSUMER_BUNDLE_OUTPUT="${4:-}" \
46+
"$BIN" > "$3" 2>&1 || true
47+
}
48+
49+
# check <log> <python assertions over `actions` (id -> action), `lines`, `out`>
50+
check() {
51+
python3 - "$1" "$2" "$3" <<'PY' || exit 1
52+
import json, sys
53+
log, out, code = sys.argv[1], sys.argv[2], sys.argv[3]
54+
lines = open(log).read().splitlines()
55+
actions = {}
56+
for l in lines:
57+
if l.startswith("mcpp:action="):
58+
a = json.loads(l[len("mcpp:action="):])
59+
actions[a["id"]] = a
60+
def fail(msg):
61+
print("FAIL: " + msg)
62+
print("--- " + log + " ---")
63+
print("\n".join(lines))
64+
sys.exit(1)
65+
exec(code)
66+
PY
67+
}
68+
69+
echo "== 1, 2. --format setup =="
70+
run_program setup "$work/setup" "$work/setup.log"
71+
check "$work/setup.log" "$work/setup" '
72+
import xml.dom.minidom, os
73+
msi = actions.get("mcpp.dist.wix")
74+
bundle = actions.get("mcpp.dist.wix.bundle")
75+
if not msi or not bundle: fail("the MSI and the bundle are not both planned: " + repr(sorted(actions)))
76+
if msi["outputs"] != [out + "/MsiConsumer-x64.msi"]: fail("the MSI is not MsiConsumer-x64.msi: " + repr(msi["outputs"]))
77+
if bundle["outputs"] != [out + "/MsiConsumer-x64.exe"]: fail("the bundle is not MsiConsumer-x64.exe: " + repr(bundle["outputs"]))
78+
if msi["outputs"][0] not in bundle["inputs"]: fail("the bundle does not take the MSI as an input")
79+
cmd = bundle["command"]
80+
ext = "WixToolset.BootstrapperApplications.wixext.dll"
81+
if "-ext" not in cmd or not cmd[cmd.index("-ext") + 1].endswith(ext): fail("the bundle command loads no extension: " + repr(cmd))
82+
if "-d" not in cmd or cmd[cmd.index("-d") + 1] != "Msi=" + msi["outputs"][0]: fail("the bundle command does not name the MSI as Msi: " + repr(cmd))
83+
consumed = set(i for a in actions.values() for i in a["inputs"])
84+
terminals = [a["id"] for a in actions.values() if not set(a["outputs"]) & consumed]
85+
if terminals != ["mcpp.dist.wix.bundle"]: fail("the bundle is not the sole terminal artifact: " + repr(terminals))
86+
for name in ("MsiConsumer.wxs", "MsiConsumer-bundle.wxs"):
87+
path = os.path.join(out, name)
88+
if not os.path.isfile(path): fail("no generated " + name)
89+
try:
90+
doc = xml.dom.minidom.parse(path)
91+
except Exception as e:
92+
fail(name + " is not well-formed XML: " + str(e))
93+
bundle_doc = open(os.path.join(out, "MsiConsumer-bundle.wxs")).read()
94+
if "bal:WixStandardBootstrapperApplication" not in bundle_doc or "<MsiPackage SourceFile=\"$(Msi)\" />" not in bundle_doc:
95+
fail("the bundle document does not chain $(Msi) under the stock bootstrapper application")
96+
'
97+
echo "ok: the bundle follows the MSI and is the sole terminal artifact, and both documents are well-formed XML"
98+
99+
echo "== 3. --format msi =="
100+
run_program msi "$work/msi" "$work/msi.log"
101+
check "$work/msi.log" "$work/msi" '
102+
if "mcpp.dist.wix" not in actions: fail("--format msi planned no MSI")
103+
if "mcpp.dist.wix.bundle" in actions: fail("--format msi planned a bundle")
104+
'
105+
echo "ok: no bundle under --format msi"
106+
107+
echo "== 4. a bundle named setup.exe =="
108+
run_program setup "$work/refused" "$work/refused.log" "$work/refused/SETUP.exe"
109+
check "$work/refused.log" "$work/refused" '
110+
if actions: fail("a bundle named setup.exe planned actions: " + repr(sorted(actions)))
111+
if not any(l.startswith("mcpp:warning=") and "WIX0388" in l for l in lines):
112+
fail("the refusal is not a warning naming WIX0388")
113+
'
114+
echo "ok: refused as a warning, before wix runs"
115+
116+
echo "PASS: dist-wix plans the MSI and the bundle, and writes well-formed documents"

0 commit comments

Comments
 (0)