3e076d4dd9
Deploy Worker / deploy (push) Failing after 1s
Shellcheck / Check shell scripts (push) Failing after 1s
CI / Build Packages (amd64 - appimage) (push) Has been skipped
CI / Build Packages (amd64 - deb) (push) Has been skipped
CI / Build Packages (amd64 - rpm) (push) Has been skipped
CI / Build Packages (arm64 - appimage) (push) Has been skipped
CI / Build Packages (arm64 - deb) (push) Has been skipped
CI / Test Flags Parsing (push) Failing after 1s
BATS Tests / BATS unit tests (push) Failing after 1s
CI / Build Packages (arm64 - rpm) (push) Has been skipped
CI / Test Build Artifacts (amd64) (push) Has been skipped
CI / Test Build Artifacts (arm64) (push) Has been skipped
CI / Update APT Repository (push) Has been cancelled
CI / Mirror Official .deb to Release (push) Has been cancelled
CI / Update DNF Repository (push) Has been cancelled
CI / Update AUR Package (push) Has been cancelled
CI / Create Release (push) Has been cancelled
257 lines
10 KiB
Bash
257 lines
10 KiB
Bash
#===============================================================================
|
|
# app.asar patch orchestration for the official Linux tree.
|
|
#
|
|
# active_patches lists the asar patch functions that still justify
|
|
# themselves against the official bytes — the patch-zero contract: the
|
|
# default verdict for any patch is delete, and when the array is empty
|
|
# the official app.asar ships byte-identical (no extract, no repack).
|
|
#
|
|
# Each entry is a function sourced from scripts/patches/*.sh that edits
|
|
# the main-process JS relative to CWD; patch_app_asar runs them with
|
|
# CWD = $app_staging_dir/resources and sets $main_js (resolved by
|
|
# _resolve_main_js — see below) as the file every patch operates on.
|
|
#
|
|
# Sourced by: build.sh
|
|
# Sourced globals:
|
|
# app_staging_dir, asar_exec, work_dir, project_root, WM_CLASS
|
|
# Modifies globals: main_js
|
|
#===============================================================================
|
|
|
|
# Survivor candidates per docs/learnings/official-deb-rebase-verification.md:
|
|
# patch_quick_window — Electron-on-KDE stale-focus bug: official
|
|
# bundle still hides without blur() (pending
|
|
# Plasma repro; drop if it doesn't reproduce)
|
|
# patch_org_plugins_path — upstream platform switch has no linux case,
|
|
# so MDM org plugins are dead on Linux without
|
|
# this (filed upstream)
|
|
# patch_virtiofsd_probe — upstream resolves virtiofsd from two paths
|
|
# plus an Ubuntu-22-only bundled fallback, so
|
|
# Cowork reports "requires QEMU" on
|
|
# Arch/Debian/Pop with a complete KVM stack
|
|
# (#771/#772; filed upstream)
|
|
# patch_cowork_bwrap — opt-in bubblewrap Cowork backend for hosts
|
|
# without KVM/vhost-vsock (ChromeOS Crostini,
|
|
# #772). Every branch is gated on
|
|
# COWORK_VM_BACKEND=bwrap, so unflagged
|
|
# launches ship the official path unchanged.
|
|
active_patches=(
|
|
patch_quick_window
|
|
patch_org_plugins_path
|
|
patch_virtiofsd_probe
|
|
patch_cowork_bwrap
|
|
)
|
|
|
|
# The #768 config-wipe guard (config.sh) is NOT wired: a contrarian
|
|
# review (see docs/learnings/config-wipe-guard.md) established that the
|
|
# primary fix is launcher-side backup rotation (backup_user_config in
|
|
# launcher-common.sh) — patch-zero-clean, out of app.asar, and covers
|
|
# the corrupt-JSON / ENOENT / single-bad-entry Zod modes an in-band
|
|
# guard misses. config.sh stays sourced-but-parked as the ready-to-arm
|
|
# fallback. Its sibling local-stores.sh was deleted outright: its
|
|
# "does-not-JSON-parse" rule missed the throwing Zod loader (WBn.parse)
|
|
# that produces spaces.json's real wipe.
|
|
|
|
# AU-1/MB-1: build-time tripwires on upstream behavior we deleted
|
|
# patches for. Each deleted patch used to WARN at patch time when its
|
|
# anchor moved; with the patches gone, an upstream flip would land
|
|
# silently. Grep the asar directly (asar stores file contents
|
|
# uncompressed) so the check also runs in patch-zero mode, where the
|
|
# archive is never extracted.
|
|
#
|
|
# managed_by_package_manager — the telemetry reason inside the
|
|
# Linux build's constant-folded updater early-return ("[updater]
|
|
# Linux: in-app updater off (updates via apt)"). Renamed by
|
|
# upstream from apt_channel_pending in the 1.18286.2 → 1.19367.0
|
|
# window when the APT channel went live: Linux updates are now
|
|
# permanently the package manager's job (decision D-001). If it
|
|
# disappears, upstream rewrote that gate and may have turned on
|
|
# self-updating, which fights the package manager — the 2.x
|
|
# autoUpdater-noop question is live again.
|
|
# menuBarEnabled:!0 — the settings default that keeps the menu bar
|
|
# on. If it disappears, upstream flipped the default the deleted
|
|
# menuBar patch used to enforce.
|
|
#
|
|
# Patterns tolerate optional whitespace so a beautified or re-minified
|
|
# bundle still matches (see CLAUDE.md, Working with Minified JavaScript).
|
|
_check_upstream_tripwires() {
|
|
local asar_path="$1"
|
|
|
|
if ! LC_ALL=C grep -aq 'managed_by_package_manager' "$asar_path"
|
|
then
|
|
echo 'Tripwire (AU-1): "managed_by_package_manager" is gone' \
|
|
'from the official bundle — upstream may have enabled the' \
|
|
'autoupdater. Re-evaluate before shipping (see' \
|
|
'docs/decisions.md D-001).' >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! LC_ALL=C grep -aqE 'menuBarEnabled:[[:space:]]*!0' "$asar_path"
|
|
then
|
|
echo 'Tripwire (MB-1): "menuBarEnabled:!0" is gone from the' \
|
|
'official bundle — upstream may have flipped the menu-bar' \
|
|
'default. Re-evaluate before shipping.' >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo 'Upstream tripwires clear (updater off on Linux, menu bar on)'
|
|
}
|
|
|
|
# Read one field out of the asar's package.json without a full extract.
|
|
_asar_package_json_field() {
|
|
local field="$1"
|
|
local asar_path="$2"
|
|
local meta_dir="$work_dir/asar-meta"
|
|
|
|
rm -rf "$meta_dir"
|
|
mkdir -p "$meta_dir" || return 1
|
|
(cd "$meta_dir" && "$asar_exec" extract-file "$asar_path" package.json) \
|
|
|| return 1
|
|
node -e 'console.log(require(process.argv[1])[process.argv[2]] ?? "")' \
|
|
"$meta_dir/package.json" "$field"
|
|
}
|
|
|
|
# Resolve the main-process JS file inside the extracted asar and echo
|
|
# its path relative to the resources CWD. Pre-3.x bundles kept the whole
|
|
# main process in .vite/build/index.js. Since upstream 1.19367.0 the
|
|
# bundle is code-split: index.js is a ~700-byte stub that require()s the
|
|
# real main chunk (index.chunk-<hash>.js — content-hashed, so the name
|
|
# changes every release). Follow the stub's require to the chunk; fall
|
|
# back to index.js for the pre-split layout. All active patches anchor on
|
|
# literals that live in this one chunk; if a future release spreads them
|
|
# across chunks, the patches need per-anchor resolution (this returns
|
|
# non-zero on a multi-chunk split rather than mispatching silently).
|
|
_resolve_main_js() {
|
|
local build_dir='app.asar.contents/.vite/build'
|
|
local stub="$build_dir/index.js"
|
|
|
|
if [[ ! -f $stub ]]; then
|
|
echo "No index.js under $build_dir — upstream layout changed?" >&2
|
|
return 1
|
|
fi
|
|
|
|
local -a chunks
|
|
mapfile -t chunks < <(
|
|
grep -oP 'require\("\./\Kindex\.chunk-[^"]+\.js(?="\))' "$stub"
|
|
)
|
|
|
|
if (( ${#chunks[@]} == 0 )); then
|
|
# Pre-split layout: index.js is the main process itself.
|
|
printf '%s\n' "$stub"
|
|
return 0
|
|
fi
|
|
if (( ${#chunks[@]} > 1 )); then
|
|
echo "index.js requires ${#chunks[@]} main chunks" \
|
|
"(${chunks[*]}) — upstream split the main bundle across" \
|
|
'files; patches need per-anchor resolution. Re-point' \
|
|
'scripts/patches/*.sh before shipping.' >&2
|
|
return 1
|
|
fi
|
|
|
|
local chunk="$build_dir/${chunks[0]}"
|
|
if [[ ! -f $chunk ]]; then
|
|
echo "index.js requires ${chunks[0]} but $chunk is missing" >&2
|
|
return 1
|
|
fi
|
|
printf '%s\n' "$chunk"
|
|
}
|
|
|
|
patch_app_asar() {
|
|
section_header 'Patch app.asar'
|
|
|
|
local resources_dir="$app_staging_dir/resources"
|
|
if [[ ! -f "$resources_dir/app.asar" ]]; then
|
|
echo "No app.asar at $resources_dir — upstream layout changed?" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Fail fast if upstream changed productName — a mismatch silently
|
|
# breaks StartupWMClass in every .desktop file we ship.
|
|
local product_name
|
|
product_name=$(_asar_package_json_field productName \
|
|
"$resources_dir/app.asar")
|
|
if [[ $product_name != "$WM_CLASS" ]]; then
|
|
echo "Error: upstream productName '$product_name' != WM_CLASS" \
|
|
"'$WM_CLASS' — update WM_CLASS in build.sh" >&2
|
|
exit 1
|
|
fi
|
|
echo "productName '$product_name' matches WM_CLASS"
|
|
|
|
# Runs against the pristine bytes, before any patch touches them.
|
|
_check_upstream_tripwires "$resources_dir/app.asar"
|
|
|
|
if (( ${#active_patches[@]} == 0 )); then
|
|
echo 'active_patches is empty — shipping the official app.asar' \
|
|
'byte-identical (patch-zero)'
|
|
section_footer 'Patch app.asar'
|
|
return 0
|
|
fi
|
|
|
|
echo "Active asar patches: ${active_patches[*]}"
|
|
cd "$resources_dir" || exit 1
|
|
"$asar_exec" extract app.asar app.asar.contents || exit 1
|
|
|
|
# Resolve the code-split main chunk once; every patch reads $main_js.
|
|
main_js=$(_resolve_main_js) || exit 1
|
|
echo "Main-process JS: $main_js"
|
|
|
|
local patch_fn
|
|
for patch_fn in "${active_patches[@]}"; do
|
|
"$patch_fn" || exit 1
|
|
done
|
|
|
|
# Repack, preserving upstream's unpacked set exactly. The unpack
|
|
# expression is derived from the shipped app.asar.unpacked tree
|
|
# rather than hardcoded, so upstream can add native helpers without
|
|
# breaking the build. asar pack honors only ONE --unpack expression,
|
|
# so every unpacked path is folded into a single brace glob.
|
|
local unpack_files unpack_glob
|
|
mapfile -t unpack_files < <(
|
|
cd app.asar.unpacked && find . -type f | sed 's|^\./||' | sort
|
|
)
|
|
if (( ${#unpack_files[@]} == 0 )); then
|
|
echo 'Warning: official app.asar.unpacked is empty —' \
|
|
'packing without --unpack'
|
|
"$asar_exec" pack app.asar.contents app.asar || exit 1
|
|
else
|
|
unpack_glob=$(IFS=,; echo "${unpack_files[*]}")
|
|
(( ${#unpack_files[@]} > 1 )) && unpack_glob="{$unpack_glob}"
|
|
"$asar_exec" pack app.asar.contents app.asar \
|
|
--unpack "$unpack_glob" || exit 1
|
|
|
|
# The repack rewrote app.asar.unpacked; diverging from the
|
|
# upstream set means a native helper got inlined (or dropped)
|
|
# and would fail at runtime.
|
|
local repacked_files
|
|
mapfile -t repacked_files < <(
|
|
cd app.asar.unpacked && find . -type f | sed 's|^\./||' | sort
|
|
)
|
|
if [[ "${unpack_files[*]}" != "${repacked_files[*]}" ]]; then
|
|
echo 'Error: repacked app.asar.unpacked diverges from the' \
|
|
'upstream unpacked set' >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# The extracted contents must not leak into the packaged tree.
|
|
rm -rf app.asar.contents
|
|
|
|
# Ship the bwrap fallback daemon beside app.asar (in resources/,
|
|
# i.e. process.resourcesPath) when its patch is active. It sits
|
|
# OUTSIDE the asar on purpose: child_process cannot exec a script
|
|
# from inside an asar, and keeping it out of app.asar.unpacked keeps
|
|
# the repack invariant above pinned to upstream's set. The file is
|
|
# inert unless the user launches with COWORK_VM_BACKEND=bwrap.
|
|
local p
|
|
for p in "${active_patches[@]}"; do
|
|
if [[ $p == 'patch_cowork_bwrap' ]]; then
|
|
cp "$project_root/scripts/cowork-fallback/cowork-vm-service.js" \
|
|
"$resources_dir/cowork-vm-service.js" || exit 1
|
|
echo 'Cowork bwrap daemon staged at resources/cowork-vm-service.js'
|
|
break
|
|
fi
|
|
done
|
|
|
|
cd "$project_root" || exit 1
|
|
section_footer 'Patch app.asar'
|
|
}
|