1243 lines
43 KiB
Bash
1243 lines
43 KiB
Bash
# shellcheck shell=bash
|
|
#
|
|
# Finding the applications and deciding which of them are Chromium underneath.
|
|
#
|
|
# The detection is deliberately conservative. A wrong "yes" appends an unknown
|
|
# argument to something that is not Chromium, and plenty of programs treat an
|
|
# unrecognised argument as a file name to open. So every rule here is a
|
|
# positive one, and anything that cannot be identified is reported as unknown
|
|
# rather than guessed at.
|
|
|
|
# Files that only ever sit next to a Chromium or Electron binary. Any single one
|
|
# is conclusive; together they cover both bundled Electron (app.asar, the
|
|
# swiftshader libraries) and plain CEF (libcef).
|
|
#
|
|
# Deliberately not in here: libEGL.so and libffmpeg.so. Chromium ships both, but
|
|
# so does the system (/usr/lib/libEGL.so exists on any machine with Mesa), and
|
|
# a marker that can be somebody else's file is not a marker.
|
|
MCA_MARKERS=(
|
|
chrome_crashpad_handler chrome-sandbox chrome_100_percent.pak
|
|
icudtl.dat v8_context_snapshot.bin snapshot_blob.bin resources.pak
|
|
libvk_swiftshader.so LICENSES.chromium.html libcef.so
|
|
)
|
|
|
|
# The markers that still mean something when the whole of a tree is searched
|
|
# rather than just the directory the binary sits in. That is the case inside a
|
|
# Flatpak, a snap or an AppImage, where there is no "next to the binary".
|
|
#
|
|
# Three of the names above are missing here on purpose. icudtl.dat is ICU's
|
|
# data file and not Chromium's: a Flutter application ships one in data/, and
|
|
# a whole-tree search would find it. snapshot_blob.bin and resources.pak are
|
|
# the same kind of shared name. Next to a binary they are still evidence,
|
|
# because nothing but Chromium unpacks its payload there. A few directories
|
|
# deeper they are not.
|
|
MCA_MARKERS_STRICT=(
|
|
chrome_crashpad_handler chrome-sandbox chrome_100_percent.pak
|
|
v8_context_snapshot.bin libvk_swiftshader.so LICENSES.chromium.html
|
|
libcef.so app.asar
|
|
)
|
|
|
|
# Shared directories, where a marker belongs to the system rather than to the
|
|
# program that happens to live there. An application ships its payload in a
|
|
# directory of its own; nothing unpacks Chromium straight into /usr/lib.
|
|
MCA_SYSTEM_DIRS=(
|
|
/ /bin /lib /lib32 /lib64 /sbin /usr /usr/bin /usr/lib /usr/lib32
|
|
/usr/lib64 /usr/libexec /usr/sbin /usr/local /usr/local/bin
|
|
/usr/local/lib /usr/local/libexec /opt
|
|
)
|
|
|
|
# The same thing for the layouts that put a machine triplet in the path.
|
|
# Debian and Ubuntu keep the shared libraries in /usr/lib/x86_64-linux-gnu
|
|
# rather than /usr/lib, so that directory is every bit as shared as /usr/lib is
|
|
# elsewhere and a marker sitting in it belongs to nobody in particular.
|
|
MCA_SYSTEM_DIR_GLOBS=(
|
|
'/usr/lib/*-linux-gnu*' '/usr/lib32/*-linux-gnu*'
|
|
'/usr/lib64/*-linux-gnu*' '/usr/local/lib/*-linux-gnu*'
|
|
)
|
|
|
|
# Where snapd mounts the installed snaps. /snap is the usual place and the one
|
|
# the shims point into; distributions that keep /snap free of a top-level
|
|
# directory use the second.
|
|
MCA_SNAP_DIRS=(/snap /var/lib/snapd/snap)
|
|
|
|
# Strings in a launcher script that mean it starts a Chromium or Electron
|
|
# process, for the wrappers whose command line is assembled out of variables and
|
|
# cannot be followed from the outside.
|
|
#
|
|
# Only strings that no other kind of program has a reason to contain. The word
|
|
# "chromium" on its own is not one of them: /usr/bin/xdg-open lists every
|
|
# browser it knows how to start, and that is not a browser.
|
|
#
|
|
# The flag file convention is one distribution's packaging habit and says
|
|
# nothing about the engine either, so it is not in here.
|
|
#
|
|
# CHROMIUM_FLAGS and CHROME_WRAPPER earn their place: they are the variables
|
|
# the Debian, Fedora and openSUSE Chromium wrappers and Google's own Chrome
|
|
# wrapper build their command line out of, and nothing else sets them.
|
|
#
|
|
# updater_bootstrap is Discord's, and it is the only thing left that names it.
|
|
# Discord no longer ships its application: /usr/bin/discord is a few lines of
|
|
# shell that run that bootstrap, which downloads Electron into the user's
|
|
# config directory on first start and execs it from there. So there is nothing
|
|
# next to the launcher to find, and the path it hands over to is built out of
|
|
# $HOME and the version it just downloaded.
|
|
MCA_SCRIPT_HINTS='ELECTRON_|app\.asar|chrome-sandbox|libcef|enable-blink-features|ozone-platform-hint|CHROMIUM_FLAGS|CHROME_WRAPPER|CHROME_VERSION_EXTRA|updater_bootstrap'
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Desktop entries
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Where Flatpak and snapd put the launchers they export. Both add these to
|
|
# XDG_DATA_DIRS themselves, through a file in /etc/profile.d. But only for a
|
|
# session that was started after they were installed, and only for a session
|
|
# manager that reads it at all. They are appended, after everything XDG names,
|
|
# so a directory that is already in the search path keeps its own position and
|
|
# the ones that were missing are still scanned.
|
|
mca_extra_desktop_dirs() {
|
|
printf '%s\n' \
|
|
"$MCA_XDG_DATA/flatpak/exports/share/applications" \
|
|
/var/lib/flatpak/exports/share/applications \
|
|
/var/lib/snapd/desktop/applications
|
|
}
|
|
|
|
# The directories a desktop entry can come from, most specific first. This is
|
|
# also XDG lookup order, so the first file found for an id is the one that is
|
|
# actually used.
|
|
mca_desktop_dirs() {
|
|
local dirs="${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" d
|
|
local -A seen=()
|
|
|
|
while IFS= read -r d; do
|
|
[[ -n $d ]] || continue
|
|
d="${d%/}"
|
|
[[ -n ${seen[$d]+set} ]] && continue
|
|
seen[$d]=1
|
|
printf '%s\n' "$d"
|
|
done < <(
|
|
printf '%s\n' "$MCA_APPDIR"
|
|
while IFS= read -r -d: d; do
|
|
[[ -n $d ]] && printf '%s/applications\n' "${d%/}"
|
|
done <<< "${dirs}:"
|
|
mca_extra_desktop_dirs
|
|
)
|
|
}
|
|
|
|
# _mca_desktop_read <file>
|
|
# Every key the scan needs, in one pass and without a single fork. There are a
|
|
# couple of hundred desktop entries on an ordinary system, and doing this with
|
|
# one awk per key per file is the difference between a menu that redraws and a
|
|
# menu that pauses.
|
|
DE_TYPE='' DE_HIDDEN='' DE_EXEC='' DE_NAME='' DE_CATEGORIES='' DE_MIME='' DE_OURS=''
|
|
|
|
_mca_desktop_read() {
|
|
local file="$1" line ingroup=0
|
|
|
|
DE_TYPE=''; DE_HIDDEN=''; DE_EXEC=''; DE_NAME=''
|
|
DE_CATEGORIES=''; DE_MIME=''; DE_OURS=''
|
|
|
|
while IFS= read -r line || [[ -n $line ]]; do
|
|
case "$line" in
|
|
'[Desktop Entry]'*) ingroup=1; continue ;;
|
|
'['*) ingroup=0; continue ;;
|
|
esac
|
|
(( ingroup )) || continue
|
|
|
|
# Locale variants are Name[de]= and never match these patterns, which
|
|
# is what we want: the untranslated key is the identifying one.
|
|
case "$line" in
|
|
Exec=*) [[ -n $DE_EXEC ]] || DE_EXEC="${line#Exec=}" ;;
|
|
Name=*) [[ -n $DE_NAME ]] || DE_NAME="${line#Name=}" ;;
|
|
Type=*) DE_TYPE="${line#Type=}" ;;
|
|
Hidden=*) DE_HIDDEN="${line#Hidden=}" ;;
|
|
Categories=*) DE_CATEGORIES="${line#Categories=}" ;;
|
|
MimeType=*) DE_MIME="${line#MimeType=}" ;;
|
|
# Only a generated shadow. An entry we edited in place carries
|
|
# X-MCA-Patched and is still the application's real entry, so it
|
|
# has to stay in the scan.
|
|
X-MCA-Generated=*) DE_OURS=1 ;;
|
|
esac
|
|
done < "$file"
|
|
}
|
|
|
|
# mca_exec_program <exec line>
|
|
# The program a desktop entry actually starts: the first token that is not an
|
|
# environment prefix, resolved to an absolute path. The result is left in
|
|
# MCA_PROG rather than printed. The scan calls this for every desktop entry on
|
|
# the system, and a command substitution each time is a fork each time.
|
|
#
|
|
# Fails for entries this tool has no safe way to rewrite: anything routed
|
|
# through a shell, where the real program is inside a quoted string.
|
|
MCA_PROG=''
|
|
|
|
mca_exec_program() {
|
|
local line="$1" tok prog=''
|
|
local -a tokens
|
|
|
|
MCA_PROG=''
|
|
|
|
# Field codes are placeholders, not arguments, and quotes only ever wrap
|
|
# whole tokens here; splitting on whitespace is enough to find token one.
|
|
read -r -a tokens <<< "$line"
|
|
|
|
for tok in "${tokens[@]}"; do
|
|
tok="${tok%\"}"; tok="${tok#\"}"
|
|
tok="${tok%\'}"; tok="${tok#\'}"
|
|
[[ -z $tok ]] && continue
|
|
[[ $tok == *=* && $tok != /* ]] && continue # VAR=value prefix
|
|
[[ $tok == env ]] && continue
|
|
prog="$tok"
|
|
break
|
|
done
|
|
|
|
[[ -n $prog ]] || return 1
|
|
|
|
case "${prog##*/}" in
|
|
sh|bash|dash|zsh|fish) return 1 ;;
|
|
esac
|
|
|
|
# PATH is searched here rather than with `command -v`, which is a builtin
|
|
# but would have to be read back through a command substitution, and that
|
|
# is a fork per desktop entry.
|
|
if [[ $prog != /* ]]; then
|
|
local d found=''
|
|
local -a pathdirs
|
|
IFS=: read -r -a pathdirs <<< "$PATH"
|
|
for d in "${pathdirs[@]}"; do
|
|
[[ -n $d ]] || continue
|
|
if [[ -x "$d/$prog" && ! -d "$d/$prog" ]]; then found="$d/$prog"; break; fi
|
|
done
|
|
[[ -n $found ]] || return 1
|
|
prog="$found"
|
|
fi
|
|
|
|
MCA_PROG="$prog"
|
|
}
|
|
|
|
# mca_exec_flatpak_id <exec line>
|
|
# The application id out of a `flatpak run ...` command line, in MCA_PROG.
|
|
mca_exec_flatpak_id() {
|
|
local line="$1" tok seen_run=0
|
|
local -a tokens
|
|
read -r -a tokens <<< "$line"
|
|
|
|
MCA_PROG=''
|
|
for tok in "${tokens[@]}"; do
|
|
if (( ! seen_run )); then
|
|
[[ $tok == run ]] && seen_run=1
|
|
continue
|
|
fi
|
|
[[ $tok == -* || $tok == @@* || $tok == %* ]] && continue
|
|
[[ $tok == *.*.* ]] || continue
|
|
MCA_PROG="$tok"
|
|
return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# mca_exec_is_steam_link <exec line> <program>
|
|
# Whether an entry starts something inside Steam rather than starting Steam
|
|
# itself: it carries a steam:// address of its own. Steam writes one of those
|
|
# for every game somebody asks for a shortcut to, and the client's own entry
|
|
# never has one. It takes an address from the outside, through %U.
|
|
mca_exec_is_steam_link() {
|
|
local line="$1" prog="$2"
|
|
|
|
[[ $line == *steam://* ]] || return 1
|
|
mca_prog_is_steam "$prog"
|
|
}
|
|
|
|
# _mca_prog_is_steam_name <program>
|
|
# The launcher under one of the names Valve and the distributions give it.
|
|
_mca_prog_is_steam_name() {
|
|
case "${1##*/}" in
|
|
steam|steam-runtime|steam-native|steam-jupiter) return 0 ;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
# _mca_prog_is_steam_wrapper <program>
|
|
# Whether a script in front of the client is a way of starting Steam. People
|
|
# put one there to add a switch of their own, and an entry pointing at it is a
|
|
# Steam start like any other. But the script is not named after Steam, so
|
|
# nothing above recognises it.
|
|
#
|
|
# Getting this wrong is worse than it sounds: such a script tends to mention
|
|
# the flag it is there to add, which is one of the markers that say "Chromium"
|
|
# to the hint scan. The entry then ends up being handled as an application and
|
|
# is given the flag on its command line, where Steam ignores it, instead of
|
|
# being handed to the Steam module that knows how to reach the web helper.
|
|
#
|
|
# Only the handover is followed, and only one step of it: the client's own
|
|
# launcher is already recognised by name, so a wrapper in front of it is the
|
|
# whole of what is left.
|
|
_mca_prog_is_steam_wrapper() {
|
|
local prog="$1" head='' target
|
|
|
|
[[ $prog == /* && -f $prog && -r $prog ]] || return 1
|
|
|
|
# Two characters, read in the shell: the scan asks this about every program
|
|
# on the system, and most of them are binaries whose first line is the whole
|
|
# file.
|
|
read -r -N 2 head < "$prog" 2>/dev/null || return 1
|
|
[[ $head == '#!' ]] || return 1
|
|
|
|
target="$(_mca_script_target "$prog")" || return 1
|
|
[[ -n $target ]] || return 1
|
|
_mca_prog_is_steam_name "$target"
|
|
}
|
|
|
|
# mca_prog_is_steam <program>
|
|
# Whether running this program starts the Steam client. Every packaging is in
|
|
# here and every name Valve and the distributions give the launcher, because
|
|
# the answer decides whether an entry gets Steam's own switch. An entry that
|
|
# starts Steam without it undoes the web helper patch on the way up.
|
|
#
|
|
# The program is what a scan leaves behind: an absolute path for a native
|
|
# install, flatpak:<id> or snap:<name> for the other two.
|
|
mca_prog_is_steam() {
|
|
local prog="$1"
|
|
|
|
_mca_prog_is_steam_name "$prog" && return 0
|
|
[[ $prog == flatpak:com.valvesoftware.Steam || $prog == snap:steam ]] && return 0
|
|
_mca_prog_is_steam_wrapper "$prog"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Is this Chromium?
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Answers are cached against size and mtime, because the systemd path unit can
|
|
# fire several times in a row while a package installs and each miss costs a
|
|
# scan of a 200 MB binary.
|
|
#
|
|
# The file is read once into memory rather than searched per lookup: a scan
|
|
# asks about every program on the system, and an awk per question is most of
|
|
# the time the scan takes.
|
|
declare -A MCA_DETECT_MEMO=()
|
|
declare -A MCA_DETECT_CACHE=()
|
|
MCA_CACHE_LOADED=0
|
|
MCA_CACHE_DIRTY=0
|
|
|
|
# Size and mtime for every program the scan is about to ask about, collected in
|
|
# one call. Checking a cache entry is still stale needs a stat, and one stat per
|
|
# program on the system was most of what a warm scan spent its time on.
|
|
declare -A MCA_STAT=()
|
|
|
|
_mca_stat_batch() {
|
|
local name st
|
|
(( $# )) || return 0
|
|
while IFS=$'\t' read -r name st; do
|
|
[[ -n $name ]] && MCA_STAT["$name"]="$st"
|
|
done < <(stat -Lc '%n %s:%Y' -- "$@" 2>/dev/null)
|
|
return 0
|
|
}
|
|
|
|
# The first line of the cache file, and the reason it is there: the verdicts
|
|
# below it are keyed on size and mtime, so an entry for a file that has not
|
|
# changed is never looked at again. A cache written when a verdict meant
|
|
# something else (before an AppImage could come out as anything but "no")
|
|
# would therefore keep answering the old way forever.
|
|
#
|
|
# So the version is part of it, and every update drops the cache. The rules
|
|
# change far more often than anyone remembers to bump a number by hand: v1.5.3
|
|
# taught the hint scan about Discord's launcher, and Discord stayed out of the
|
|
# list anyway, because /usr/bin/discord had not changed and its old "no" was
|
|
# still in here.
|
|
MCA_CACHE_FORMAT="# middleclick-autoscroll detect 2 $MCA_VERSION"
|
|
|
|
_mca_cache_load() {
|
|
local path stamp verdict first=1
|
|
|
|
(( MCA_CACHE_LOADED )) && return 0
|
|
MCA_CACHE_LOADED=1
|
|
|
|
[[ -r "$MCA_CACHEDIR/detect" ]] || return 0
|
|
while IFS=$'\t' read -r path stamp verdict; do
|
|
if (( first )); then
|
|
first=0
|
|
[[ $path == "$MCA_CACHE_FORMAT" ]] || return 0
|
|
continue
|
|
fi
|
|
[[ -n $path && -n $stamp ]] || continue
|
|
MCA_DETECT_CACHE["$path"]="$stamp"$'\t'"$verdict"
|
|
done < "$MCA_CACHEDIR/detect"
|
|
return 0
|
|
}
|
|
|
|
# Written back once, at exit, instead of after every miss.
|
|
mca_cache_flush() {
|
|
local path entry tmp
|
|
|
|
(( MCA_CACHE_DIRTY )) || return 0
|
|
mkdir -p "$MCA_CACHEDIR" 2>/dev/null || return 0
|
|
|
|
tmp="$(mktemp "$MCA_CACHEDIR/detect.XXXXXX")" || return 0
|
|
printf '%s\n' "$MCA_CACHE_FORMAT" > "$tmp"
|
|
for path in "${!MCA_DETECT_CACHE[@]}"; do
|
|
entry="${MCA_DETECT_CACHE[$path]}"
|
|
printf '%s\t%s\n' "$path" "$entry" >> "$tmp"
|
|
done
|
|
mv -f "$tmp" "$MCA_CACHEDIR/detect" 2>/dev/null || rm -f "$tmp"
|
|
MCA_CACHE_DIRTY=0
|
|
return 0
|
|
}
|
|
|
|
# _mca_has_markers <directory>
|
|
_mca_has_markers() {
|
|
local dir="$1" m s
|
|
|
|
[[ -d $dir ]] || return 1
|
|
dir="${dir%/}"
|
|
for s in "${MCA_SYSTEM_DIRS[@]}"; do
|
|
[[ $dir == "$s" ]] && return 1
|
|
done
|
|
for s in "${MCA_SYSTEM_DIR_GLOBS[@]}"; do
|
|
# Unquoted on purpose: these are patterns, not names.
|
|
# shellcheck disable=SC2053
|
|
[[ $dir == $s ]] && return 1
|
|
done
|
|
|
|
for m in "${MCA_MARKERS[@]}"; do
|
|
[[ -e "$dir/$m" ]] && return 0
|
|
done
|
|
[[ -e "$dir/resources/app.asar" || -e "$dir/app.asar" ]] && return 0
|
|
return 1
|
|
}
|
|
|
|
# _mca_find_markers <directory> <depth>
|
|
# The same question for a whole tree, which is the shape a Flatpak, a snap and
|
|
# an unpacked AppImage come in: everything the application ships is somewhere
|
|
# under one root and there is no single directory that is "next to the binary".
|
|
# Hence the narrower list, see MCA_MARKERS_STRICT.
|
|
_mca_find_markers() {
|
|
local root="$1" depth="$2" m
|
|
local -a names=()
|
|
|
|
[[ -d $root ]] || return 1
|
|
|
|
for m in "${MCA_MARKERS_STRICT[@]}"; do
|
|
(( ${#names[@]} )) && names+=(-o)
|
|
names+=(-name "$m")
|
|
done
|
|
|
|
[[ -n "$(find "$root" -maxdepth "$depth" \
|
|
\( "${names[@]}" \) -print -quit 2>/dev/null)" ]]
|
|
}
|
|
|
|
# The plain assignments the script made before it handed over, for
|
|
# _mca_script_subst to read. A variable of its own rather than something passed
|
|
# around: every caller of _mca_script_target reads it through a command
|
|
# substitution, so each call already works on a copy and there is nothing here
|
|
# that two of them could collide over.
|
|
declare -A MCA_SCRIPT_VARS=()
|
|
|
|
# _mca_script_subst <text>
|
|
# The text with $NAME and ${NAME} replaced by what the script assigned to them,
|
|
# left in MCA_SUBST.
|
|
#
|
|
# Fails as soon as something turns up that only a running shell could work out,
|
|
# like a positional parameter, a name the script never set or a default value.
|
|
# That is the point: an unresolvable path has to come out as no path at all, never
|
|
# as a wrong one.
|
|
MCA_SUBST=''
|
|
|
|
_mca_script_subst() {
|
|
local text="$1" out='' rest name
|
|
|
|
while [[ $text == *'$'* ]]; do
|
|
out+="${text%%\$*}"
|
|
rest="${text#*\$}"
|
|
|
|
if [[ $rest == '{'* ]]; then
|
|
[[ $rest == *'}'* ]] || return 1
|
|
name="${rest%%\}*}"; name="${name#\{}"
|
|
rest="${rest#*\}}"
|
|
else
|
|
name="${rest%%[!A-Za-z0-9_]*}"
|
|
rest="${rest:${#name}}"
|
|
fi
|
|
|
|
[[ $name =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || return 1
|
|
[[ -n ${MCA_SCRIPT_VARS[$name]+set} ]] || return 1
|
|
|
|
out+="${MCA_SCRIPT_VARS[$name]}"
|
|
text="$rest"
|
|
done
|
|
|
|
MCA_SUBST="$out$text"
|
|
return 0
|
|
}
|
|
|
|
# _mca_script_target <script>
|
|
# The program a wrapper script hands over to, so a chain like
|
|
# heroic -> electron43 -> /usr/lib/electron43/electron can be followed.
|
|
#
|
|
# The assignments above the exec line are followed as well, because that is the
|
|
# shape the Chromium wrappers outside Arch have: Debian, Ubuntu, Fedora and
|
|
# openSUSE all set the directory and the program name into variables at the top
|
|
# of the script and end on `exec -a "$APPNAME" "$LIBDIR/$APPNAME"`. Without
|
|
# resolving those there is nothing to follow, and the answer would have to come
|
|
# from the hint scan, which is weaker evidence than finding the binary and its
|
|
# markers.
|
|
_mca_script_target() {
|
|
local script="$1" line tok name val skip=0
|
|
local -a tokens
|
|
local re_assign='^[[:space:]]*(export[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)=([^[:space:];|&()`]*)[[:space:]]*$'
|
|
|
|
MCA_SCRIPT_VARS=()
|
|
|
|
# Only assignments and exec lines matter, and the shell is slow at reading
|
|
# a big script line by line. So grep picks them out, and a script with no
|
|
# exec at all (winetricks has 20000 lines of that) is not read here at all.
|
|
grep -qaE '^[[:space:]]*exec[[:space:]]' -- "$script" 2>/dev/null || return 1
|
|
|
|
while IFS= read -r line; do
|
|
if [[ $line =~ $re_assign ]]; then
|
|
name="${BASH_REMATCH[2]}"
|
|
val="${BASH_REMATCH[3]}"
|
|
|
|
# One pair of quotes around the whole value is ordinary and means
|
|
# nothing here. Single quotes also mean the value is literal, so
|
|
# there is nothing left to expand.
|
|
if [[ $val == \'*\' ]]; then
|
|
MCA_SCRIPT_VARS[$name]="${val:1:${#val}-2}"
|
|
continue
|
|
fi
|
|
[[ $val == \"*\" ]] && val="${val:1:${#val}-2}"
|
|
|
|
if _mca_script_subst "$val"; then
|
|
MCA_SCRIPT_VARS[$name]="$MCA_SUBST"
|
|
else
|
|
# Not resolvable, so anything built from it must not be either.
|
|
unset "MCA_SCRIPT_VARS[$name]"
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
[[ $line =~ ^[[:space:]]*exec[[:space:]]+(.*)$ ]] || continue
|
|
read -r -a tokens <<< "${BASH_REMATCH[1]}"
|
|
skip=0
|
|
for tok in "${tokens[@]}"; do
|
|
(( skip )) && { skip=0; continue; }
|
|
case "$tok" in
|
|
env) continue ;;
|
|
# `exec -a NAME PROG` renames the process. NAME is not a
|
|
# program, and it is usually the wrapper's own name, so
|
|
# following it would lead straight back here.
|
|
-a|--argv0) skip=1; continue ;;
|
|
-*) continue ;;
|
|
esac
|
|
[[ $tok == *=* && $tok != /* ]] && continue
|
|
|
|
tok="${tok%\"}"; tok="${tok#\"}"
|
|
tok="${tok%\'}"; tok="${tok#\'}"
|
|
|
|
# Anything still carrying a shell variable is resolved from the
|
|
# assignments above, or not at all; the hint scan answers for the
|
|
# wrappers that build their command line some other way.
|
|
if [[ $tok == *'$'* ]]; then
|
|
_mca_script_subst "$tok" || return 1
|
|
tok="$MCA_SUBST"
|
|
fi
|
|
[[ -n $tok ]] || return 1
|
|
|
|
if [[ $tok == /* ]]; then
|
|
printf '%s\n' "$tok"
|
|
else
|
|
command -v "$tok" 2>/dev/null
|
|
fi
|
|
return $?
|
|
done
|
|
done < <(grep -aE '^[[:space:]]*((export[[:space:]]+)?[A-Za-z_][A-Za-z0-9_]*=|exec[[:space:]])' \
|
|
-- "$script" 2>/dev/null)
|
|
return 1
|
|
}
|
|
|
|
# mca_flags_candidates <program>
|
|
# The flag files a launcher reads, in the order it reads them, taken from the
|
|
# launcher itself rather than assumed from the distribution. Arch's Electron
|
|
# and Chromium packages follow that convention and take extra arguments from
|
|
# $XDG_CONFIG_HOME/<name>-flags.conf. Where the wrappers do that, it is a far
|
|
# better place to inject than a desktop entry: it survives package updates and it
|
|
# applies to a launch from the terminal too. Where they do not, this finds
|
|
# nothing and the caller falls back to the desktop entry.
|
|
mca_flags_candidates() {
|
|
local prog="$1" depth="${2:-0}" target inherited
|
|
(( depth > 3 )) && return 0
|
|
[[ -r $prog ]] || return 0
|
|
head -c2 -- "$prog" 2>/dev/null | grep -q '#!' || return 0
|
|
|
|
grep -oE '[A-Za-z0-9_.+-]+-flags\.conf' -- "$prog" 2>/dev/null
|
|
|
|
# A wrapper that only execs another wrapper (heroic -> electron43) inherits
|
|
# that one's flag files, plus the specific name it builds from a variable
|
|
# at runtime and therefore never writes down. That is why that one is
|
|
# derived from the target's own name rather than found.
|
|
#
|
|
# It is derived only once the target has shown that it reads a flag file at
|
|
# all. Every launcher that can be followed is not a launcher that reads
|
|
# one: the Chromium wrappers outside Arch do not, and neither does an
|
|
# ordinary program that simply execs its own binary. Naming a file for
|
|
# those would put the flag somewhere nothing ever looks and skip the
|
|
# desktop entry that would have worked.
|
|
if target="$(_mca_script_target "$prog")" && [[ -n $target ]]; then
|
|
inherited="$(mca_flags_candidates "$target" $(( depth + 1 )))"
|
|
if [[ -n $inherited ]]; then
|
|
printf '%s-flags.conf\n' "${target##*/}"
|
|
printf '%s\n' "$inherited"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Looking inside an AppImage
|
|
# ---------------------------------------------------------------------------
|
|
#
|
|
# An AppImage is the AppImage runtime (an ordinary ELF executable) with a
|
|
# squashfs image appended to it. The payload is compressed, so nothing the
|
|
# application ships is on disk where the marker check could find it, and for a
|
|
# long time that made every AppImage a "cannot tell".
|
|
#
|
|
# It does not have to be. squashfs keeps the names of everything it holds in
|
|
# one table of its own, the table is a few kilobytes, and the names are the
|
|
# whole of what this question needs. So that table is read and inflated here -
|
|
# rather than unpacking a hundred megabytes, and rather than running the image
|
|
# to ask it, which is the one thing a scan started by a path unit must never
|
|
# do.
|
|
#
|
|
# Everything below fails closed. The moment a field is not where it should be,
|
|
# or the compressor is one there is no tool for, the answer goes back to
|
|
# "cannot tell" and the application is left to the applications screen.
|
|
|
|
# _mca_bytes <file> <offset> <count>
|
|
# Count bytes from an offset, as numbers, in MCA_BYTES. od rather than a shell
|
|
# read because a variable cannot hold a NUL byte and these headers are full of
|
|
# them. And a whole header at a time rather than a field at a time, because
|
|
# the first question below is asked about every program on the system and one
|
|
# od per field would be four forks per program.
|
|
MCA_BYTES=()
|
|
|
|
_mca_bytes() {
|
|
local file="$1" off="$2" count="$3"
|
|
|
|
MCA_BYTES=()
|
|
|
|
# read stops on end of input rather than on the delimiter it was given, so
|
|
# it reports failure for input that is perfectly complete. The count is
|
|
# the check.
|
|
read -r -d '' -a MCA_BYTES \
|
|
< <(od -An -tu1 -j"$off" -N"$count" -v -- "$file" 2>/dev/null)
|
|
|
|
(( ${#MCA_BYTES[@]} == count ))
|
|
}
|
|
|
|
# _mca_slice <file> <offset> <length>
|
|
# Length bytes from an offset, on stdout.
|
|
_mca_slice() {
|
|
dd if="$1" bs=65536 iflag=skip_bytes,count_bytes \
|
|
skip="$2" count="$3" status=none 2>/dev/null
|
|
}
|
|
|
|
# _mca_word <index>
|
|
# One 64-bit little-endian field out of MCA_BYTES, in MCA_WORD. Assigns rather
|
|
# than prints for the same reason _mca_bytes reads a whole header at a time:
|
|
# printing means a command substitution, and that is a fork per field.
|
|
#
|
|
# Fails for the all-ones value squashfs writes down for a table that is not in
|
|
# the image, which is a case the callers below have to tell from a real offset
|
|
# and bash arithmetic (signed, 64-bit) cannot represent.
|
|
MCA_WORD=0
|
|
|
|
_mca_word() {
|
|
local at="$1"
|
|
|
|
(( ${#MCA_BYTES[@]} >= at + 8 )) || return 1
|
|
(( MCA_BYTES[at+7] == 255 && MCA_BYTES[at+6] == 255 )) && return 1
|
|
|
|
MCA_WORD=$(( (MCA_BYTES[at] | MCA_BYTES[at+1] << 8 \
|
|
| MCA_BYTES[at+2] << 16 | MCA_BYTES[at+3] << 24) \
|
|
| ((MCA_BYTES[at+4] | MCA_BYTES[at+5] << 8 \
|
|
| MCA_BYTES[at+6] << 16 | MCA_BYTES[at+7] << 24) << 32) ))
|
|
return 0
|
|
}
|
|
|
|
# _mca_appimage_offset <file>
|
|
# Where the appended image starts, in MCA_APPIMAGE_AT: directly after the ELF,
|
|
# which is where its section header table ends. This is how the runtime works
|
|
# it out for --appimage-offset, and it is exact.
|
|
#
|
|
# Searching for the squashfs magic instead would not be: the runtime carries a
|
|
# copy of it in its own code. Neither is the AppImage magic at byte 8 of the
|
|
# ELF header a way in. It is there to be zeroed, and a build pipeline that
|
|
# does not want its image picked up by a desktop integration daemon does
|
|
# exactly that. So the offset is computed for any ELF at all and the caller
|
|
# finds out whether there is an image at it.
|
|
MCA_APPIMAGE_AT=0
|
|
|
|
_mca_appimage_offset() {
|
|
local shoff shentsize shnum
|
|
|
|
MCA_APPIMAGE_AT=0
|
|
_mca_bytes "$1" 0 64 || return 1
|
|
|
|
# \x7fELF, and then the class byte: 2 for the 64-bit header, 1 for the
|
|
# 32-bit one, which puts every field after it somewhere else.
|
|
(( MCA_BYTES[0] == 127 && MCA_BYTES[1] == 69 \
|
|
&& MCA_BYTES[2] == 76 && MCA_BYTES[3] == 70 )) || return 1
|
|
|
|
if (( MCA_BYTES[4] == 2 )); then
|
|
_mca_word 40 || return 1
|
|
shoff="$MCA_WORD"
|
|
shentsize=$(( MCA_BYTES[58] | MCA_BYTES[59] << 8 ))
|
|
shnum=$(( MCA_BYTES[60] | MCA_BYTES[61] << 8 ))
|
|
elif (( MCA_BYTES[4] == 1 )); then
|
|
shoff=$(( MCA_BYTES[32] | MCA_BYTES[33] << 8 \
|
|
| MCA_BYTES[34] << 16 | MCA_BYTES[35] << 24 ))
|
|
shentsize=$(( MCA_BYTES[46] | MCA_BYTES[47] << 8 ))
|
|
shnum=$(( MCA_BYTES[48] | MCA_BYTES[49] << 8 ))
|
|
else
|
|
return 1
|
|
fi
|
|
|
|
(( shoff > 0 && shentsize > 0 && shnum > 0 )) || return 1
|
|
MCA_APPIMAGE_AT=$(( shoff + shentsize * shnum ))
|
|
return 0
|
|
}
|
|
|
|
# _mca_inflate <compressor> <file> <offset> <length>
|
|
# One squashfs metadata block, decompressed onto stdout.
|
|
#
|
|
# lzo and lz4 are missing because squashfs stores them as bare blocks and
|
|
# neither lzop nor the lz4 tool will read one without the framing their own
|
|
# file format puts around it. An image compressed with either comes out as
|
|
# "cannot tell", which is where it started.
|
|
_mca_inflate() {
|
|
local comp="$1" file="$2" off="$3" len="$4"
|
|
|
|
case "$comp" in
|
|
1)
|
|
# squashfs stores a zlib stream and gzip only reads its own
|
|
# container, but underneath both are the same deflate data with a
|
|
# different wrapper around it. So the wrapper is swapped: zlib's
|
|
# two header bytes are dropped and a minimal gzip header put in
|
|
# front. gzip then writes every byte of the block and complains
|
|
# about the trailer it did not get, which is why its status is
|
|
# thrown away here and the check is on the output instead.
|
|
(( len > 2 )) || return 1
|
|
{
|
|
printf '\037\213\010\000\000\000\000\000\000\003'
|
|
_mca_slice "$file" $(( off + 2 )) $(( len - 2 ))
|
|
} | { gzip -dc 2>/dev/null || true; }
|
|
;;
|
|
2)
|
|
mca_have xz || return 1
|
|
_mca_slice "$file" "$off" "$len" \
|
|
| { xz -dc --format=lzma 2>/dev/null || true; }
|
|
;;
|
|
4)
|
|
mca_have xz || return 1
|
|
_mca_slice "$file" "$off" "$len" | { xz -dc 2>/dev/null || true; }
|
|
;;
|
|
6)
|
|
mca_have zstd || return 1
|
|
_mca_slice "$file" "$off" "$len" | { zstd -dc 2>/dev/null || true; }
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# _mca_squashfs_names <file> <offset>
|
|
# The directory table of the image at that offset, inflated onto stdout. It is
|
|
# not parsed: the names sit in it as plain text between the records that
|
|
# describe them, and a name is all the caller is looking for.
|
|
#
|
|
# Fails unless the walk lands exactly on the end of the table. That is the
|
|
# integrity check: the block sizes adding up to the table's own length is what
|
|
# says the fields were read from a real superblock, and that the output is the
|
|
# whole of the names rather than some of them. It is also why the caller has
|
|
# to collect this before trusting it, never pipe it.
|
|
_mca_squashfs_names() {
|
|
local file="$1" base="$2"
|
|
local comp dir_start end field header size pos
|
|
|
|
_mca_bytes "$file" "$base" 96 || return 1
|
|
comp=$(( MCA_BYTES[20] | MCA_BYTES[21] << 8 ))
|
|
|
|
_mca_word 72 || return 1
|
|
dir_start="$MCA_WORD"
|
|
(( dir_start > 0 )) || return 1
|
|
|
|
# Where the names stop: the first table squashfs writes after them. Which
|
|
# one that is depends on the image. There is no fragment table when
|
|
# nothing was packed into a fragment, and no export table unless it was
|
|
# asked for. So they are tried in the order they are written and the
|
|
# first one that is actually there wins. bytes_used closes the list for an
|
|
# image that has none of them.
|
|
end=0
|
|
for field in 80 88 48 40; do
|
|
_mca_word "$field" || continue
|
|
(( MCA_WORD > dir_start )) || continue
|
|
end="$MCA_WORD"
|
|
break
|
|
done
|
|
(( end > dir_start )) || return 1
|
|
|
|
# Names for a hundred thousand files would still fit in a fraction of
|
|
# this. A table that claims more than it is a table that was misread.
|
|
(( end - dir_start > 8388608 )) && return 1
|
|
|
|
pos=$(( base + dir_start ))
|
|
end=$(( base + end ))
|
|
|
|
while (( pos < end )); do
|
|
_mca_bytes "$file" "$pos" 2 || return 1
|
|
header=$(( MCA_BYTES[0] | MCA_BYTES[1] << 8 ))
|
|
size=$(( header & 0x7fff ))
|
|
(( size > 0 && pos + 2 + size <= end )) || return 1
|
|
|
|
# The top bit says the block was stored as it is, which squashfs does
|
|
# for the ones compression made no smaller.
|
|
if (( header & 0x8000 )); then
|
|
_mca_slice "$file" $(( pos + 2 )) "$size" || return 1
|
|
else
|
|
_mca_inflate "$comp" "$file" $(( pos + 2 )) "$size" || return 1
|
|
fi
|
|
|
|
pos=$(( pos + 2 + size ))
|
|
done
|
|
|
|
(( pos == end ))
|
|
}
|
|
|
|
# _mca_appimage_verdict <file>
|
|
# What the image appended to this file contains, left in MCA_APPIMAGE:
|
|
# yes - Chromium, CEF or Electron
|
|
# no - the names of everything inside were read and none of them is
|
|
# unknown - there is an image, but its contents could not be read
|
|
# none - not an AppImage; there is nothing appended to this ELF
|
|
MCA_APPIMAGE=none
|
|
|
|
_mca_appimage_verdict() {
|
|
local file="$1" base names m
|
|
local -a args=()
|
|
|
|
MCA_APPIMAGE=none
|
|
|
|
_mca_appimage_offset "$file" || return 0
|
|
base="$MCA_APPIMAGE_AT"
|
|
|
|
# hsqs, the squashfs magic. Nothing there means nothing was appended, so
|
|
# this is an ordinary executable and not an AppImage at all.
|
|
_mca_bytes "$file" "$base" 4 || return 0
|
|
(( MCA_BYTES[0] == 104 && MCA_BYTES[1] == 115 \
|
|
&& MCA_BYTES[2] == 113 && MCA_BYTES[3] == 115 )) || return 0
|
|
|
|
# From here on there is an image, so the worst this can end on is "cannot
|
|
# tell". Never "no", which would be an answer about contents that were
|
|
# never read.
|
|
MCA_APPIMAGE=unknown
|
|
|
|
names="$(mktemp "${TMPDIR:-/tmp}/mca-names.XXXXXX")" || return 0
|
|
|
|
# Nothing at all in the table means every block failed to inflate (a
|
|
# compressor whose tool is not installed). That is a "cannot tell" too.
|
|
if _mca_squashfs_names "$file" "$base" > "$names" 2>/dev/null \
|
|
&& [[ -s $names ]]
|
|
then
|
|
for m in "${MCA_MARKERS_STRICT[@]}"; do args+=(-e "$m"); done
|
|
if grep -qaF "${args[@]}" -- "$names"; then
|
|
MCA_APPIMAGE=yes
|
|
else
|
|
MCA_APPIMAGE=no
|
|
fi
|
|
fi
|
|
|
|
rm -f -- "$names"
|
|
return 0
|
|
}
|
|
|
|
# mca_detect_verdict <program>
|
|
# What running this program starts, left in MCA_VERDICT:
|
|
# yes - Chromium, CEF or Electron
|
|
# no - something else
|
|
# unknown - an image whose payload could not be read, so neither answer has
|
|
# been earned and the applications screen offers it as a choice
|
|
#
|
|
# Assigns rather than returns three states through an exit code, and is the one
|
|
# place the memo and the on-disk cache are consulted.
|
|
MCA_VERDICT=''
|
|
|
|
mca_detect_verdict() {
|
|
local prog="$1" real stamp cached
|
|
|
|
MCA_VERDICT=no
|
|
[[ -n $prog && -e $prog ]] || return 0
|
|
|
|
# Memoized under the path as given, so the same launcher named twice in a
|
|
# scan costs nothing at all the second time.
|
|
if [[ -n ${MCA_DETECT_MEMO[$prog]+set} ]]; then
|
|
MCA_VERDICT="${MCA_DETECT_MEMO[$prog]}"
|
|
return 0
|
|
fi
|
|
|
|
_mca_cache_load
|
|
|
|
if [[ -n ${MCA_STAT[$prog]+set} ]]; then
|
|
stamp="${MCA_STAT[$prog]}"
|
|
else
|
|
stamp="$(stat -Lc '%s:%Y' -- "$prog" 2>/dev/null)" || stamp=''
|
|
fi
|
|
|
|
if [[ -n $stamp && -n ${MCA_DETECT_CACHE[$prog]+set} ]]; then
|
|
cached="${MCA_DETECT_CACHE[$prog]}"
|
|
if [[ "${cached%%$'\t'*}" == "$stamp" ]]; then
|
|
MCA_VERDICT="${cached#*$'\t'}"
|
|
MCA_DETECT_MEMO[$prog]="$MCA_VERDICT"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
real="$(readlink -f -- "$prog" 2>/dev/null)" || real="$prog"
|
|
|
|
MCA_DETECT_UNSURE=0
|
|
if _mca_detect_uncached "$real"; then
|
|
MCA_VERDICT=yes
|
|
elif (( MCA_DETECT_UNSURE )); then
|
|
MCA_VERDICT=unknown
|
|
else
|
|
MCA_VERDICT=no
|
|
fi
|
|
|
|
MCA_DETECT_MEMO[$prog]="$MCA_VERDICT"
|
|
if [[ -n $stamp ]]; then
|
|
MCA_DETECT_CACHE["$prog"]="$stamp"$'\t'"$MCA_VERDICT"
|
|
MCA_CACHE_DIRTY=1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# mca_is_chromium <program>
|
|
# Succeeds when the program is a Chromium, CEF or Electron process. "Cannot
|
|
# tell" is not that, so it fails here. Anything that has to treat the two
|
|
# differently asks mca_detect_verdict instead.
|
|
mca_is_chromium() {
|
|
mca_detect_verdict "$1"
|
|
[[ $MCA_VERDICT == yes ]]
|
|
}
|
|
|
|
# Set by _mca_detect_uncached when it reaches the end without finding anything
|
|
# and the reason is that something could not be read, rather than that there
|
|
# was nothing there. Only mca_detect_verdict reads it, straight after the call.
|
|
MCA_DETECT_UNSURE=0
|
|
|
|
_mca_detect_uncached() {
|
|
local real="$1" depth="${2:-0}" dir target
|
|
|
|
(( depth > 3 )) && return 1
|
|
[[ -r $real ]] || return 1
|
|
|
|
if head -c2 -- "$real" 2>/dev/null | grep -q '#!'; then
|
|
# A launcher script. Following where it hands over is the reliable
|
|
# answer; the hint scan catches the ones that build the command line
|
|
# out of variables (vesktop, discord and most vendor wrappers).
|
|
if target="$(_mca_script_target "$real")" && [[ -n $target ]]; then
|
|
_mca_detect_uncached "$(readlink -f -- "$target" 2>/dev/null || printf '%s' "$target")" \
|
|
$(( depth + 1 )) && return 0
|
|
fi
|
|
grep -qE "$MCA_SCRIPT_HINTS" -- "$real" 2>/dev/null && return 0
|
|
return 1
|
|
fi
|
|
|
|
# A binary. Everything Chromium ships is unpacked next to it, either in the
|
|
# same directory or (for /opt/thing/bin/Thing layouts) one level up.
|
|
dir="$(dirname -- "$real")"
|
|
_mca_has_markers "$dir" && return 0
|
|
[[ ${dir##*/} == bin ]] && _mca_has_markers "${dir%/*}" && return 0
|
|
|
|
# An AppImage keeps all of that inside a filesystem appended to itself, so
|
|
# there is nothing next to the binary to find. But the names of everything
|
|
# in there can be read, and that settles it either way.
|
|
_mca_appimage_verdict "$real"
|
|
case "$MCA_APPIMAGE" in
|
|
yes) return 0 ;;
|
|
no) return 1 ;;
|
|
esac
|
|
|
|
# Last resort: Chromium's own argument table is in the binary. -m1 stops at
|
|
# the first hit, so this reads far less than the file size suggests.
|
|
grep -qaFm1 -- 'enable-blink-features' "$real" 2>/dev/null && return 0
|
|
grep -qaFm1 -- 'CHROME_VERSION_EXTRA' "$real" 2>/dev/null && return 0
|
|
|
|
# What is left is an image that could not be read (an unsupported
|
|
# compressor) or the older AppImage layout, which is an ISO9660 filesystem
|
|
# and has no name table of this shape at all. Either way the answer is not
|
|
# "no", it is "nobody looked".
|
|
if [[ $MCA_APPIMAGE == unknown || $real == *.AppImage || $real == *.appimage ]]; then
|
|
MCA_DETECT_UNSURE=1
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The scan
|
|
# ---------------------------------------------------------------------------
|
|
# Results land in parallel arrays rather than being printed, so the caller can
|
|
# use them for both patching and the applications screen without scanning twice.
|
|
|
|
MCA_IDS=() # desktop file id, without the .desktop suffix
|
|
MCA_FILES=() # the desktop entry that is in effect for that id
|
|
MCA_NAMES=() # display name
|
|
MCA_PROGS=() # resolved program, or a Flatpak app id or a snap name
|
|
MCA_KINDS=() # app | browser | steam | spotify | unknown | no
|
|
MCA_PACKAGING=() # native | flatpak | snap
|
|
|
|
# A scan reads every desktop entry on the system, so the menu does it once and
|
|
# then redraws from what it found. Applying rescans on its own, so nothing else
|
|
# has to remember to invalidate this.
|
|
MCA_SCANNED=0
|
|
|
|
mca_scan_once() {
|
|
(( MCA_SCANNED )) && return 0
|
|
mca_scan
|
|
}
|
|
|
|
mca_scan() {
|
|
local dir file id name exec_line prog kind packaging i
|
|
local -A seen=()
|
|
local -a c_ids=() c_files=() c_names=() c_progs=() c_browser=() c_stat=()
|
|
|
|
MCA_IDS=(); MCA_FILES=(); MCA_NAMES=(); MCA_PROGS=(); MCA_KINDS=()
|
|
MCA_PACKAGING=()
|
|
|
|
# Pass one: read the entries and work out what each of them starts. No
|
|
# detection yet: that needs a stat per program, and those are collected so
|
|
# they can be asked for all at once.
|
|
while IFS= read -r dir; do
|
|
[[ -d $dir ]] || continue
|
|
for file in "$dir"/*.desktop; do
|
|
[[ -f $file ]] || continue
|
|
|
|
id="${file##*/}"; id="${id%.desktop}"
|
|
[[ -n ${seen[$id]+set} ]] && continue
|
|
seen[$id]=1
|
|
|
|
_mca_desktop_read "$file"
|
|
|
|
# One of our own generated entries. It describes the same
|
|
# application as the system one it shadows, so it is skipped and
|
|
# the id left free for the original further down the search path.
|
|
[[ -n $DE_OURS ]] && { unset "seen[$id]"; continue; }
|
|
|
|
[[ $DE_TYPE == Application ]] || continue
|
|
[[ $DE_HIDDEN == true ]] && continue
|
|
|
|
exec_line="$DE_EXEC"
|
|
[[ -n $exec_line ]] || continue
|
|
|
|
name="$DE_NAME"
|
|
[[ -n $name ]] || name="$id"
|
|
|
|
mca_exec_program "$exec_line" || continue
|
|
prog="$MCA_PROG"
|
|
|
|
if [[ ${prog##*/} == flatpak ]]; then
|
|
mca_exec_flatpak_id "$exec_line" || continue
|
|
prog="flatpak:$MCA_PROG"
|
|
elif mca_snap_name "$prog"; then
|
|
prog="snap:$MCA_PROG"
|
|
fi
|
|
|
|
# The shortcuts Steam writes for single games are not
|
|
# applications of their own. A game is whatever engine it was
|
|
# built with, and none of those reads a Chromium argument. And
|
|
# starting the client through one needs nothing on its command
|
|
# line either.
|
|
mca_exec_is_steam_link "$exec_line" "$prog" && continue
|
|
|
|
c_ids+=("$id"); c_files+=("$file"); c_names+=("$name")
|
|
c_progs+=("$prog")
|
|
if mca_desktop_is_browser; then c_browser+=(1); else c_browser+=(0); fi
|
|
[[ $prog == /* ]] && c_stat+=("$prog")
|
|
done
|
|
done < <(mca_desktop_dirs)
|
|
|
|
MCA_STAT=()
|
|
_mca_stat_batch "${c_stat[@]}"
|
|
|
|
# Pass two: decide what each one is. What it does (an application or a
|
|
# browser) and how it was packaged are two separate questions: a Chromium
|
|
# installed as a snap is still a browser, and somebody who has turned
|
|
# browsers off means that one too.
|
|
for i in "${!c_ids[@]}"; do
|
|
prog="${c_progs[i]}"
|
|
kind=no
|
|
packaging=native
|
|
|
|
if [[ $prog == flatpak:* ]]; then
|
|
packaging=flatpak
|
|
if mca_prog_is_steam "$prog"; then
|
|
kind=steam
|
|
elif mca_flatpak_is_chromium "${prog#flatpak:}"; then
|
|
(( c_browser[i] )) && kind=browser || kind=app
|
|
fi
|
|
prog="${prog#flatpak:}"
|
|
elif [[ $prog == snap:* ]]; then
|
|
prog="${prog#snap:}"
|
|
packaging=snap
|
|
if [[ $prog == steam ]]; then
|
|
kind=steam
|
|
elif mca_snap_is_chromium "$prog"; then
|
|
(( c_browser[i] )) && kind=browser || kind=app
|
|
fi
|
|
elif mca_prog_is_steam "$prog"; then
|
|
# Steam is Chromium inside, but nothing about it can be changed
|
|
# from a command line argument; it has its own module.
|
|
kind=steam
|
|
elif [[ ${prog##*/} == spotify-launcher ]]; then
|
|
# Not Chromium itself, but it starts Spotify, which is CEF. It
|
|
# takes the flag from its own configuration file.
|
|
kind=spotify
|
|
else
|
|
mca_detect_verdict "$prog"
|
|
case "$MCA_VERDICT" in
|
|
yes) (( c_browser[i] )) && kind=browser || kind=app ;;
|
|
unknown) kind=unknown ;;
|
|
esac
|
|
fi
|
|
|
|
[[ $kind == no ]] && continue
|
|
|
|
MCA_IDS+=("${c_ids[i]}")
|
|
MCA_FILES+=("${c_files[i]}")
|
|
MCA_NAMES+=("${c_names[i]}")
|
|
MCA_PROGS+=("$prog")
|
|
MCA_KINDS+=("$kind")
|
|
MCA_PACKAGING+=("$packaging")
|
|
done
|
|
|
|
mca_cache_flush
|
|
MCA_SCANNED=1
|
|
mca_config_migrate
|
|
}
|
|
|
|
# mca_has_flags_file <program>
|
|
# Whether the program's launcher reads a flag file. Memoized: the status block
|
|
# asks this for every application it lists, and answering it means reading the
|
|
# launcher script.
|
|
declare -A MCA_FLAGS_MEMO=()
|
|
|
|
mca_has_flags_file() {
|
|
local prog="$1"
|
|
|
|
if [[ -z ${MCA_FLAGS_MEMO[$prog]+set} ]]; then
|
|
if [[ -n "$(mca_flags_candidates "$prog")" ]]; then
|
|
MCA_FLAGS_MEMO[$prog]=yes
|
|
else
|
|
MCA_FLAGS_MEMO[$prog]=no
|
|
fi
|
|
fi
|
|
|
|
[[ ${MCA_FLAGS_MEMO[$prog]} == yes ]]
|
|
}
|
|
|
|
# A browser is anything that offers itself for http. That is the property that
|
|
# matters here: those are the applications where middle click currently pastes
|
|
# a URL, so a user may well want them left alone.
|
|
#
|
|
# Reads the keys _mca_desktop_read left behind, so it only makes sense straight
|
|
# after that call.
|
|
mca_desktop_is_browser() {
|
|
[[ $DE_CATEGORIES == *WebBrowser* ]] && return 0
|
|
[[ $DE_MIME == *x-scheme-handler/http* ]] && return 0
|
|
return 1
|
|
}
|
|
|
|
# mca_snap_name <program>
|
|
# The snap an executable belongs to, left in MCA_PROG. It is assigned rather
|
|
# than printed for the same reason mca_exec_program is: the scan asks this about
|
|
# every desktop entry on the system, and a command substitution per entry is a
|
|
# fork per entry.
|
|
#
|
|
# /snap/bin/<name> is the shim snapd puts in PATH and is a symlink to snapd
|
|
# itself, so following it lands on /usr/bin/snap and says nothing whatever
|
|
# about the application. The name is the only thing that carries information,
|
|
# and it is what leads to the mounted tree below.
|
|
mca_snap_name() {
|
|
local prog="$1" rest d
|
|
|
|
for d in "${MCA_SNAP_DIRS[@]}"; do
|
|
if [[ $prog == "$d/bin/"* ]]; then
|
|
rest="${prog#"$d/bin/"}"
|
|
# A snap that ships several programs names them <snap>.<app>.
|
|
MCA_PROG="${rest%%.*}"
|
|
return 0
|
|
fi
|
|
if [[ $prog == "$d/"* ]]; then
|
|
rest="${prog#"$d/"}"
|
|
MCA_PROG="${rest%%/*}"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# mca_snap_is_chromium <snap name>
|
|
# A snap keeps everything it ships inside its own mounted revision, so the
|
|
# marker check works the same way there as anywhere else once that tree has
|
|
# been located. "current" is the symlink snapd keeps pointing at the revision
|
|
# that will actually be started.
|
|
mca_snap_is_chromium() {
|
|
local name="$1" d root
|
|
|
|
if [[ -n ${MCA_DETECT_MEMO[snap:$name]+set} ]]; then
|
|
[[ ${MCA_DETECT_MEMO[snap:$name]} == yes ]]
|
|
return $?
|
|
fi
|
|
|
|
for d in "${MCA_SNAP_DIRS[@]}"; do
|
|
root="$d/$name/current"
|
|
if _mca_find_markers "$root" 5; then
|
|
MCA_DETECT_MEMO[snap:$name]=yes
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
MCA_DETECT_MEMO[snap:$name]=no
|
|
return 1
|
|
}
|
|
|
|
# mca_flatpak_is_chromium <app id>
|
|
# Flatpak keeps every application in its own tree, so the marker check works the
|
|
# same way once that tree has been located.
|
|
mca_flatpak_is_chromium() {
|
|
local id="$1" loc
|
|
mca_have flatpak || return 1
|
|
|
|
if [[ -n ${MCA_DETECT_MEMO[flatpak:$id]+set} ]]; then
|
|
[[ ${MCA_DETECT_MEMO[flatpak:$id]} == yes ]]
|
|
return $?
|
|
fi
|
|
|
|
loc="$(flatpak info --show-location "$id" 2>/dev/null)"
|
|
if [[ -n $loc ]] && _mca_find_markers "$loc/files" 4; then
|
|
MCA_DETECT_MEMO[flatpak:$id]=yes
|
|
return 0
|
|
fi
|
|
|
|
MCA_DETECT_MEMO[flatpak:$id]=no
|
|
return 1
|
|
}
|
|
|
|
# mca_kind_wanted <kind> <id>
|
|
# Whether this entry should be patched. Skip beats everything, an explicit
|
|
# include beats detection, and detection beats nothing.
|
|
mca_kind_wanted() {
|
|
local kind="$1" id="$2"
|
|
|
|
mca_config_list_has Skip "$id" && return 1
|
|
mca_config_list_has Include "$id" && return 0
|
|
|
|
[[ $kind == app || $kind == browser ]]
|
|
}
|