@@ -483,9 +483,14 @@ rl_sign_windows() { # rl_sign_windows <stagedir-or-file> [...]
483483
484484# ------------------------------------------------------------------- NSIS ---
485485#
486- # Two shapes of Windows installer:
487- # --cli installs into Program Files and appends to the system PATH
488- # --gui <exe> the above plus Start Menu and Desktop shortcuts
486+ # Three shapes of Windows installer:
487+ # --plain installs into Program Files; touches nothing else
488+ # --cli the above plus an append to the system PATH
489+ # --gui <exe> --plain plus Start Menu and Desktop shortcuts
490+ #
491+ # Pick --cli only for something the user types at a prompt. A plugin loaded by a
492+ # host application is --plain: putting it on PATH gains nothing and, before the
493+ # guard below existed, cost one reporter their entire system PATH (nib#2).
489494#
490495# Both write an uninstaller and the Add/Remove Programs registry keys. The file
491496# list is generated from the staging directory so callers never hand-maintain
595600 printf ' %s' " $work /Uninstall.exe"
596601}
597602
598- rl_nsis () { # rl_nsis <label> <stagedir> --cli | --gui <exe>
603+ rl_nsis () { # rl_nsis <label> <stagedir> --plain | -- cli | --gui <exe>
599604 # RL_EULA (optional, from rl_eula) adds a licence page. Required when the NDI
600605 # runtime is bundled — that is the condition Vizrt's redistribution grant
601606 # rests on, so it is not cosmetic.
@@ -650,6 +655,11 @@ rl_nsis() { # rl_nsis <label> <stagedir> --cli | --gui <exe>
650655 fi
651656
652657 local shortcuts=" " unshortcuts=" " pathblock=" " unpathblock=" "
658+ # ☠️ This used to be `if --gui ... else <write the system PATH>`, so EVERY mode
659+ # that was not --gui got the PATH block — including the ~33 Resolume plugins,
660+ # which drop a .dll into a plugin folder and have no business on PATH at all.
661+ # That is how nib#2 (a wiped system PATH) reached a user. Modes are explicit
662+ # now, and an unknown one is a hard error rather than "probably CLI".
653663 if [[ " $mode " == " --gui" ]]; then
654664 shortcuts=$( cat << SC
655665 CreateDirectory "\$ SMPROGRAMS\\ ${RL_NAME} "
663673 Delete "\$ DESKTOP\\ ${RL_NAME} .lnk"
664674SC
665675)
666- else
676+ elif [[ " $mode " == " --plain" ]]; then
677+ # Installs files, an uninstaller and the Add/Remove Programs keys, and
678+ # touches nothing else. This is what a plugin wants.
679+ :
680+ elif [[ " $mode " == " --cli" ]]; then
667681 # CLI: put the install dir on the machine PATH via EnvVarUpdate-lite.
668682 pathblock=$( cat << 'SC '
669- ; Append to the system PATH (idempotent: only if not already present)
683+ ; Append to the system PATH (idempotent: only if not already present).
684+ ;
685+ ; ☠️ ReadRegStr returns "" AND sets the error flag when the value is longer
686+ ; than NSIS_MAX_STRLEN -- 1024 in the stock makensis build, and a real
687+ ; Windows system PATH very often exceeds it. Writing "$0;$INSTDIR" on that
688+ ; empty $0 does not append, it REPLACES the whole system PATH with this one
689+ ; directory: System32 included, so `ping` and everything else stops
690+ ; resolving. That is nib#2. A build machine's PATH is short, which is why it
691+ ; survived testing. Never write unless the read demonstrably succeeded and
692+ ; came back non-empty.
693+ ClearErrors
670694 ReadRegStr $0 HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path"
695+ IfErrors pathdone
696+ StrCmp $0 "" pathdone
671697 Push $0
672698 Push "$INSTDIR"
673699 Call StrStr
678704 pathdone:
679705SC
680706)
707+ else
708+ echo " rl_nsis: unknown mode '${mode} ' (expected --cli, --gui <exe>, or --plain)" >&2
709+ return 1
681710 fi
682711
683712 # The uninstall section is assembled separately because the signed-uninstaller
@@ -878,12 +907,58 @@ rl_notary_args() {
878907# Helpers/chrome_crashpad_handler in there, and codesign validates
879908# subcomponents, so signing the version directory fails with "code object is
880909# not signed at all" until that binary is signed first.
910+ # The JIT exceptions, for a loose binary that needs them. Written once per run,
911+ # next to the other scratch files.
912+ rl_jit_entitlements () {
913+ local f=" ${RL_OUT_DIR:- ${TMPDIR:-/ tmp} } /rl-jit-entitlements.plist"
914+ [[ -f " $f " ]] && { printf ' %s' " $f " ; return 0; }
915+ mkdir -p " $( dirname " $f " ) "
916+ cat > " $f " << 'PLIST '
917+ <?xml version="1.0" encoding="UTF-8"?>
918+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
919+ <plist version="1.0">
920+ <dict>
921+ <key>com.apple.security.cs.allow-jit</key><true/>
922+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
923+ <key>com.apple.security.cs.disable-library-validation</key><true/>
924+ </dict>
925+ </plist>
926+ PLIST
927+ printf ' %s' " $f "
928+ }
929+
930+ # Does this loose binary carry a JIT of its own? V8's symbol strings say so for
931+ # a bundled Node runtime, which is the case that matters here.
932+ rl_is_jit_binary () { # rl_is_jit_binary <file>
933+ # LC_ALL=C: a 100 MB runtime is full of bytes that are not valid UTF-8, and
934+ # grep in a UTF-8 locale abandons the whole file rather than the line.
935+ LC_ALL=C grep -qa ' v8::internal::' " $1 " 2> /dev/null
936+ }
937+
881938rl_mac_sign_loose () { # rl_mac_sign_loose <root> [<path-prefix-to-skip>]
882- local root=" $1 " skip=" ${2:- } " f
939+ local root=" $1 " skip=" ${2:- } " f ent
883940 while IFS= read -r f; do
884941 [[ -n " $skip " && " $f " == $skip * ]] && continue
885942 rl_grep ' Mach-O' " $( file -b " $f " 2> /dev/null || true) " || continue
886- codesign --force --options runtime --timestamp \
943+ # Entitlements do NOT flow from the enclosing bundle to a binary signed on
944+ # its own, and they are read per *process*: a launcher that spawns
945+ # Contents/Resources/node gets node's entitlements for that process, not
946+ # the app's. So an embedded runtime signed bare aborts on its first JIT
947+ # allocation while the app around it verifies and notarises perfectly —
948+ # BlackMatrix shipped three releases that way, dying with SIGTRAP before it
949+ # could print a line. A JIT-carrying loose binary is signed with the JIT
950+ # exceptions: the run's own entitlements when it has some, or the standard
951+ # set when it does not.
952+ ent=()
953+ if rl_is_jit_binary " $f " ; then
954+ if [[ -n " ${RL_MAC_ENTITLEMENTS:- } " && -f " ${RL_MAC_ENTITLEMENTS:- } " ]]; then
955+ ent=(--entitlements " $RL_MAC_ENTITLEMENTS " )
956+ else
957+ ent=(--entitlements " $( rl_jit_entitlements) " )
958+ fi
959+ rl_note " JIT runtime, entitled: ${f# $root / } "
960+ fi
961+ codesign --force --options runtime --timestamp ${ent[@]+" ${ent[@]} " } \
887962 --sign " $RL_MAC_SIGN_IDENTITY " " $f " 2> " $RL_CS_ERR " > /dev/null \
888963 || { echo " codesign failed on nested $f : $( cat " $RL_CS_ERR " ) " >&2 ; return 1; }
889964 done < <( find " $root " \( -type d \( -name ' *.app' -o -name ' *.framework' \) \) -prune \
@@ -984,6 +1059,40 @@ rl_mac_sign() { # rl_mac_sign <path-to-.app-or-binary>
9841059 # executable on its own makes codesign validate the whole enclosing bundle
9851060 # early — which fails on a PyInstaller app, where Contents/Frameworks holds
9861061 # base_library.zip and codesign treats that as unsigned nested code.
1062+ #
1063+ # ...with one exception: a SIDECAR. Tauri's `externalBin` puts helper
1064+ # binaries in Contents/MacOS alongside the main executable, and the notary
1065+ # service rejects those — the bundle seal covers them for integrity, but
1066+ # each is its own Mach-O and needs its own Developer ID signature, hardened
1067+ # runtime and secure timestamp. Burrow v0.1.0 was refused with exactly
1068+ # that, three times over, for Contents/MacOS/burrow-helper:
1069+ #
1070+ # The binary is not signed with a valid Developer ID certificate.
1071+ # The signature does not include a secure timestamp.
1072+ # The executable does not have the hardened runtime enabled.
1073+ #
1074+ # Burrow is the fleet's first app with a sidecar, which is why this has
1075+ # never come up before. The main executable is still left alone, so the
1076+ # PyInstaller reasoning above is untouched.
1077+ #
1078+ # The comparison is exact rather than a prefix: rl_mac_sign_loose's skip
1079+ # argument is a prefix match, and "$app/Contents/MacOS/burrow" is a prefix
1080+ # of "burrow-helper" — reusing it here would skip the very binary this
1081+ # exists to sign.
1082+ local mainexe sidecar
1083+ mainexe=" $( /usr/libexec/PlistBuddy -c ' Print :CFBundleExecutable' \
1084+ " $app /Contents/Info.plist" 2> /dev/null || true) "
1085+ if [[ -n " $mainexe " ]]; then
1086+ while IFS= read -r sidecar; do
1087+ [[ " $sidecar " == " $app /Contents/MacOS/$mainexe " ]] && continue
1088+ rl_grep ' Mach-O' " $( file -b " $sidecar " 2> /dev/null || true) " || continue
1089+ rl_note " sidecar: ${sidecar# $app / } "
1090+ codesign --force --options runtime --timestamp \
1091+ --sign " $RL_MAC_SIGN_IDENTITY " " $sidecar " \
1092+ 2> " $RL_CS_ERR " > /dev/null \
1093+ || { echo " codesign failed on sidecar $sidecar : $( cat " $RL_CS_ERR " ) " >&2 ; return 1; }
1094+ done < <( find " $app /Contents/MacOS" -maxdepth 1 -type f 2> /dev/null | sort)
1095+ fi
9871096 fi
9881097 # ${a[@]+"${a[@]}"}: bash 3.2 — still /bin/bash on macOS, and what launchd
9891098 # runs — treats "${a[@]}" on an EMPTY array as an unbound variable under
0 commit comments